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

codeigniter4 / CodeIgniter4 / 21507617415

30 Jan 2026 07:11AM UTC coverage: 85.382% (-0.1%) from 85.527%
21507617415

push

github

web-flow
feat: FrankenPHP Worker Mode (#9889)

Co-authored-by: John Paul E. Balandan, CPA <paulbalandan@gmail.com>
Co-authored-by: neznaika0 <ozornick.ks@gmail.com>

153 of 243 new or added lines in 19 files covered. (62.96%)

1 existing line in 1 file now uncovered.

22119 of 25906 relevant lines covered (85.38%)

205.24 hits per line

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

85.88
/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 stdClass;
20
use Stringable;
21
use Throwable;
22

23
/**
24
 * @property-read array      $aliasedTables
25
 * @property-read string     $charset
26
 * @property-read bool       $compress
27
 * @property-read float      $connectDuration
28
 * @property-read float      $connectTime
29
 * @property-read string     $database
30
 * @property-read array      $dateFormat
31
 * @property-read string     $DBCollat
32
 * @property-read bool       $DBDebug
33
 * @property-read string     $DBDriver
34
 * @property-read string     $DBPrefix
35
 * @property-read string     $DSN
36
 * @property-read array|bool $encrypt
37
 * @property-read array      $failover
38
 * @property-read string     $hostname
39
 * @property-read Query      $lastQuery
40
 * @property-read string     $password
41
 * @property-read bool       $pConnect
42
 * @property-read int|string $port
43
 * @property-read bool       $pretend
44
 * @property-read string     $queryClass
45
 * @property-read array      $reservedIdentifiers
46
 * @property-read bool       $strictOn
47
 * @property-read string     $subdriver
48
 * @property-read string     $swapPre
49
 * @property-read int        $transDepth
50
 * @property-read bool       $transFailure
51
 * @property-read bool       $transStatus
52
 * @property-read string     $username
53
 *
54
 * @template TConnection
55
 * @template TResult
56
 *
57
 * @implements ConnectionInterface<TConnection, TResult>
58
 * @see \CodeIgniter\Database\BaseConnectionTest
59
 */
60
abstract class BaseConnection implements ConnectionInterface
61
{
62
    /**
63
     * Data Source Name / Connect string
64
     *
65
     * @var string
66
     */
67
    protected $DSN;
68

69
    /**
70
     * Database port
71
     *
72
     * @var int|string
73
     */
74
    protected $port = '';
75

76
    /**
77
     * Hostname
78
     *
79
     * @var string
80
     */
81
    protected $hostname;
82

83
    /**
84
     * Username
85
     *
86
     * @var string
87
     */
88
    protected $username;
89

90
    /**
91
     * Password
92
     *
93
     * @var string
94
     */
95
    protected $password;
96

97
    /**
98
     * Database name
99
     *
100
     * @var string
101
     */
102
    protected $database;
103

104
    /**
105
     * Database driver
106
     *
107
     * @var string
108
     */
109
    protected $DBDriver = 'MySQLi';
110

111
    /**
112
     * Sub-driver
113
     *
114
     * @used-by CI_DB_pdo_driver
115
     *
116
     * @var string
117
     */
118
    protected $subdriver;
119

120
    /**
121
     * Table prefix
122
     *
123
     * @var string
124
     */
125
    protected $DBPrefix = '';
126

127
    /**
128
     * Persistent connection flag
129
     *
130
     * @var bool
131
     */
132
    protected $pConnect = false;
133

134
    /**
135
     * Whether to throw Exception or not when an error occurs.
136
     *
137
     * @var bool
138
     */
139
    protected $DBDebug = true;
140

141
    /**
142
     * Character set
143
     *
144
     * This value must be updated by Config\Database if the driver use it.
145
     *
146
     * @var string
147
     */
148
    protected $charset = '';
149

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

159
    /**
160
     * Swap Prefix
161
     *
162
     * @var string
163
     */
164
    protected $swapPre = '';
165

166
    /**
167
     * Encryption flag/data
168
     *
169
     * @var array|bool
170
     */
171
    protected $encrypt = false;
172

173
    /**
174
     * Compression flag
175
     *
176
     * @var bool
177
     */
178
    protected $compress = false;
179

180
    /**
181
     * Strict ON flag
182
     *
183
     * Whether we're running in strict SQL mode.
184
     *
185
     * @var bool|null
186
     *
187
     * @deprecated 4.5.0 Will move to MySQLi\Connection.
188
     */
189
    protected $strictOn;
190

191
    /**
192
     * Settings for a failover connection.
193
     *
194
     * @var array
195
     */
196
    protected $failover = [];
197

198
    /**
199
     * The last query object that was executed
200
     * on this connection.
201
     *
202
     * @var Query
203
     */
204
    protected $lastQuery;
205

206
    /**
207
     * Connection ID
208
     *
209
     * @var false|TConnection
210
     */
211
    public $connID = false;
212

213
    /**
214
     * Result ID
215
     *
216
     * @var false|TResult
217
     */
218
    public $resultID = false;
219

220
    /**
221
     * Protect identifiers flag
222
     *
223
     * @var bool
224
     */
225
    public $protectIdentifiers = true;
226

227
    /**
228
     * List of reserved identifiers
229
     *
230
     * Identifiers that must NOT be escaped.
231
     *
232
     * @var array
233
     */
234
    protected $reservedIdentifiers = ['*'];
235

236
    /**
237
     * Identifier escape character
238
     *
239
     * @var array|string
240
     */
241
    public $escapeChar = '"';
242

243
    /**
244
     * ESCAPE statement string
245
     *
246
     * @var string
247
     */
248
    public $likeEscapeStr = " ESCAPE '%s' ";
249

250
    /**
251
     * ESCAPE character
252
     *
253
     * @var string
254
     */
255
    public $likeEscapeChar = '!';
256

257
    /**
258
     * RegExp used to escape identifiers
259
     *
260
     * @var array
261
     */
262
    protected $pregEscapeChar = [];
263

264
    /**
265
     * Holds previously looked up data
266
     * for performance reasons.
267
     *
268
     * @var array
269
     */
270
    public $dataCache = [];
271

272
    /**
273
     * Microtime when connection was made
274
     *
275
     * @var float
276
     */
277
    protected $connectTime = 0.0;
278

279
    /**
280
     * How long it took to establish connection.
281
     *
282
     * @var float
283
     */
284
    protected $connectDuration = 0.0;
285

286
    /**
287
     * If true, no queries will actually be
288
     * run against the database.
289
     *
290
     * @var bool
291
     */
292
    protected $pretend = false;
293

294
    /**
295
     * Transaction enabled flag
296
     *
297
     * @var bool
298
     */
299
    public $transEnabled = true;
300

301
    /**
302
     * Strict transaction mode flag
303
     *
304
     * @var bool
305
     */
306
    public $transStrict = true;
307

308
    /**
309
     * Transaction depth level
310
     *
311
     * @var int
312
     */
313
    protected $transDepth = 0;
314

315
    /**
316
     * Transaction status flag
317
     *
318
     * Used with transactions to determine if a rollback should occur.
319
     *
320
     * @var bool
321
     */
322
    protected $transStatus = true;
323

324
    /**
325
     * Transaction failure flag
326
     *
327
     * Used with transactions to determine if a transaction has failed.
328
     *
329
     * @var bool
330
     */
331
    protected $transFailure = false;
332

333
    /**
334
     * Whether to throw exceptions during transaction
335
     */
336
    protected bool $transException = false;
337

338
    /**
339
     * Array of table aliases.
340
     *
341
     * @var list<string>
342
     */
343
    protected $aliasedTables = [];
344

345
    /**
346
     * Query Class
347
     *
348
     * @var string
349
     */
350
    protected $queryClass = Query::class;
351

352
    /**
353
     * Default Date/Time formats
354
     *
355
     * @var array<string, string>
356
     */
357
    protected array $dateFormat = [
358
        'date'        => 'Y-m-d',
359
        'datetime'    => 'Y-m-d H:i:s',
360
        'datetime-ms' => 'Y-m-d H:i:s.v',
361
        'datetime-us' => 'Y-m-d H:i:s.u',
362
        'time'        => 'H:i:s',
363
    ];
364

365
    /**
366
     * Saves our connection settings.
367
     */
368
    public function __construct(array $params)
369
    {
370
        if (isset($params['dateFormat'])) {
426✔
371
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
103✔
372
            unset($params['dateFormat']);
103✔
373
        }
374

375
        foreach ($params as $key => $value) {
426✔
376
            if (property_exists($this, $key)) {
135✔
377
                $this->{$key} = $value;
135✔
378
            }
379
        }
380

381
        $queryClass = str_replace('Connection', 'Query', static::class);
426✔
382

383
        if (class_exists($queryClass)) {
426✔
384
            $this->queryClass = $queryClass;
354✔
385
        }
386

387
        if ($this->failover !== []) {
426✔
388
            // If there is a failover database, connect now to do failover.
389
            // Otherwise, Query Builder creates SQL statement with the main database config
390
            // (DBPrefix) even when the main database is down.
391
            $this->initialize();
2✔
392
        }
393
    }
394

395
    /**
396
     * Initializes the database connection/settings.
397
     *
398
     * @return void
399
     *
400
     * @throws DatabaseException
401
     */
402
    public function initialize()
403
    {
404
        /* If an established connection is available, then there's
405
         * no need to connect and select the database.
406
         *
407
         * Depending on the database driver, connID can be either
408
         * boolean TRUE, a resource or an object.
409
         */
410
        if ($this->connID) {
819✔
411
            return;
783✔
412
        }
413

414
        $this->connectTime = microtime(true);
50✔
415
        $connectionErrors  = [];
50✔
416

417
        try {
418
            // Connect to the database and set the connection ID
419
            $this->connID = $this->connect($this->pConnect);
50✔
420
        } catch (Throwable $e) {
2✔
421
            $this->connID       = false;
2✔
422
            $connectionErrors[] = sprintf(
2✔
423
                'Main connection [%s]: %s',
2✔
424
                $this->DBDriver,
2✔
425
                $e->getMessage(),
2✔
426
            );
2✔
427
            log_message('error', 'Error connecting to the database: ' . $e);
2✔
428
        }
429

430
        // No connection resource? Check if there is a failover else throw an error
431
        if (! $this->connID) {
50✔
432
            // Check if there is a failover set
433
            if (! empty($this->failover) && is_array($this->failover)) {
4✔
434
                // Go over all the failovers
435
                foreach ($this->failover as $index => $failover) {
2✔
436
                    // Replace the current settings with those of the failover
437
                    foreach ($failover as $key => $val) {
2✔
438
                        if (property_exists($this, $key)) {
2✔
439
                            $this->{$key} = $val;
2✔
440
                        }
441
                    }
442

443
                    try {
444
                        // Try to connect
445
                        $this->connID = $this->connect($this->pConnect);
2✔
446
                    } catch (Throwable $e) {
1✔
447
                        $connectionErrors[] = sprintf(
1✔
448
                            'Failover #%d [%s]: %s',
1✔
449
                            ++$index,
1✔
450
                            $this->DBDriver,
1✔
451
                            $e->getMessage(),
1✔
452
                        );
1✔
453
                        log_message('error', 'Error connecting to the database: ' . $e);
1✔
454
                    }
455

456
                    // If a connection is made break the foreach loop
457
                    if ($this->connID) {
2✔
458
                        break;
2✔
459
                    }
460
                }
461
            }
462

463
            // We still don't have a connection?
464
            if (! $this->connID) {
4✔
465
                throw new DatabaseException(sprintf(
2✔
466
                    'Unable to connect to the database.%s%s',
2✔
467
                    PHP_EOL,
2✔
468
                    implode(PHP_EOL, $connectionErrors),
2✔
469
                ));
2✔
470
            }
471
        }
472

473
        $this->connectDuration = microtime(true) - $this->connectTime;
48✔
474
    }
475

476
    /**
477
     * Close the database connection.
478
     *
479
     * @return void
480
     */
481
    public function close()
482
    {
483
        if ($this->connID) {
3✔
484
            $this->_close();
3✔
485
            $this->connID = false;
3✔
486
        }
487
    }
488

489
    /**
490
     * Keep or establish the connection if no queries have been sent for
491
     * a length of time exceeding the server's idle timeout.
492
     *
493
     * @return void
494
     */
495
    public function reconnect()
496
    {
497
        if ($this->ping() === false) {
2✔
498
            $this->close();
1✔
499
            $this->initialize();
1✔
500
        }
501
    }
502

503
    /**
504
     * Platform dependent way method for closing the connection.
505
     *
506
     * @return void
507
     */
508
    abstract protected function _close();
509

510
    /**
511
     * Check if the connection is still alive.
512
     */
513
    public function ping(): bool
514
    {
515
        if ($this->connID === false) {
5✔
516
            return false;
2✔
517
        }
518

519
        return $this->_ping();
4✔
520
    }
521

522
    /**
523
     * Driver-specific ping implementation.
524
     */
525
    protected function _ping(): bool
526
    {
527
        try {
528
            $result = $this->simpleQuery('SELECT 1');
4✔
529

530
            return $result !== false;
4✔
NEW
531
        } catch (DatabaseException) {
×
NEW
532
            return false;
×
533
        }
534
    }
535

536
    /**
537
     * Create a persistent database connection.
538
     *
539
     * @return false|TConnection
540
     */
541
    public function persistentConnect()
542
    {
543
        return $this->connect(true);
×
544
    }
545

546
    /**
547
     * Returns the actual connection object. If both a 'read' and 'write'
548
     * connection has been specified, you can pass either term in to
549
     * get that connection. If you pass either alias in and only a single
550
     * connection is present, it must return the sole connection.
551
     *
552
     * @return false|TConnection
553
     */
554
    public function getConnection(?string $alias = null)
555
    {
556
        // @todo work with read/write connections
557
        return $this->connID;
2✔
558
    }
559

560
    /**
561
     * Returns the name of the current database being used.
562
     */
563
    public function getDatabase(): string
564
    {
565
        return empty($this->database) ? '' : $this->database;
765✔
566
    }
567

568
    /**
569
     * Set DB Prefix
570
     *
571
     * Set's the DB Prefix to something new without needing to reconnect
572
     *
573
     * @param string $prefix The prefix
574
     */
575
    public function setPrefix(string $prefix = ''): string
576
    {
577
        return $this->DBPrefix = $prefix;
13✔
578
    }
579

580
    /**
581
     * Returns the database prefix.
582
     */
583
    public function getPrefix(): string
584
    {
585
        return $this->DBPrefix;
12✔
586
    }
587

588
    /**
589
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
590
     */
591
    public function getPlatform(): string
592
    {
593
        return $this->DBDriver;
23✔
594
    }
595

596
    /**
597
     * Sets the Table Aliases to use. These are typically
598
     * collected during use of the Builder, and set here
599
     * so queries are built correctly.
600
     *
601
     * @return $this
602
     */
603
    public function setAliasedTables(array $aliases)
604
    {
605
        $this->aliasedTables = $aliases;
986✔
606

607
        return $this;
986✔
608
    }
609

610
    /**
611
     * Add a table alias to our list.
612
     *
613
     * @return $this
614
     */
615
    public function addTableAlias(string $alias)
616
    {
617
        if ($alias === '') {
30✔
618
            return $this;
6✔
619
        }
620

621
        if (! in_array($alias, $this->aliasedTables, true)) {
24✔
622
            $this->aliasedTables[] = $alias;
24✔
623
        }
624

625
        return $this;
24✔
626
    }
627

628
    /**
629
     * Executes the query against the database.
630
     *
631
     * @return false|TResult
632
     */
633
    abstract protected function execute(string $sql);
634

635
    /**
636
     * Orchestrates a query against the database. Queries must use
637
     * Database\Statement objects to store the query and build it.
638
     * This method works with the cache.
639
     *
640
     * Should automatically handle different connections for read/write
641
     * queries if needed.
642
     *
643
     * @param array|string|null $binds
644
     *
645
     * @return BaseResult<TConnection, TResult>|bool|Query
646
     *
647
     * @todo BC set $queryClass default as null in 4.1
648
     */
649
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
650
    {
651
        $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass;
818✔
652

653
        if (empty($this->connID)) {
818✔
654
            $this->initialize();
24✔
655
        }
656

657
        /**
658
         * @var Query $query
659
         */
660
        $query = new $queryClass($this);
818✔
661

662
        $query->setQuery($sql, $binds, $setEscapeFlags);
818✔
663

664
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
818✔
665
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
666
        }
667

668
        $startTime = microtime(true);
818✔
669

670
        // Always save the last query so we can use
671
        // the getLastQuery() method.
672
        $this->lastQuery = $query;
818✔
673

674
        // If $pretend is true, then we just want to return
675
        // the actual query object here. There won't be
676
        // any results to return.
677
        if ($this->pretend) {
818✔
678
            $query->setDuration($startTime);
10✔
679

680
            return $query;
10✔
681
        }
682

683
        // Run the query for real
684
        try {
685
            $exception      = null;
818✔
686
            $this->resultID = $this->simpleQuery($query->getQuery());
818✔
687
        } catch (DatabaseException $exception) {
14✔
688
            $this->resultID = false;
14✔
689
        }
690

691
        if ($this->resultID === false) {
818✔
692
            $query->setDuration($startTime, $startTime);
33✔
693

694
            // This will trigger a rollback if transactions are being used
695
            $this->handleTransStatus();
33✔
696

697
            if (
698
                $this->DBDebug
33✔
699
                && (
700
                    // Not in transactions
701
                    $this->transDepth === 0
33✔
702
                    // In transactions, do not throw exception by default.
33✔
703
                    || $this->transException
33✔
704
                )
705
            ) {
706
                // We call this function in order to roll-back queries
707
                // if transactions are enabled. If we don't call this here
708
                // the error message will trigger an exit, causing the
709
                // transactions to remain in limbo.
710
                while ($this->transDepth !== 0) {
10✔
711
                    $transDepth = $this->transDepth;
2✔
712
                    $this->transComplete();
2✔
713

714
                    if ($transDepth === $this->transDepth) {
2✔
715
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
716
                        break;
×
717
                    }
718
                }
719

720
                // Let others do something with this query.
721
                Events::trigger('DBQuery', $query);
10✔
722

723
                if ($exception instanceof DatabaseException) {
10✔
724
                    throw new DatabaseException(
8✔
725
                        $exception->getMessage(),
8✔
726
                        $exception->getCode(),
8✔
727
                        $exception,
8✔
728
                    );
8✔
729
                }
730

731
                return false;
2✔
732
            }
733

734
            // Let others do something with this query.
735
            Events::trigger('DBQuery', $query);
23✔
736

737
            return false;
23✔
738
        }
739

740
        $query->setDuration($startTime);
818✔
741

742
        // Let others do something with this query
743
        Events::trigger('DBQuery', $query);
818✔
744

745
        // resultID is not false, so it must be successful
746
        if ($this->isWriteType($sql)) {
818✔
747
            return true;
779✔
748
        }
749

750
        // query is not write-type, so it must be read-type query; return QueryResult
751
        $resultClass = str_replace('Connection', 'Result', static::class);
817✔
752

753
        return new $resultClass($this->connID, $this->resultID);
817✔
754
    }
755

756
    /**
757
     * Performs a basic query against the database. No binding or caching
758
     * is performed, nor are transactions handled. Simply takes a raw
759
     * query string and returns the database-specific result id.
760
     *
761
     * @return false|TResult
762
     */
763
    public function simpleQuery(string $sql)
764
    {
765
        if (empty($this->connID)) {
825✔
766
            $this->initialize();
6✔
767
        }
768

769
        return $this->execute($sql);
825✔
770
    }
771

772
    /**
773
     * Disable Transactions
774
     *
775
     * This permits transactions to be disabled at run-time.
776
     *
777
     * @return void
778
     */
779
    public function transOff()
780
    {
781
        $this->transEnabled = false;
×
782
    }
783

784
    /**
785
     * Enable/disable Transaction Strict Mode
786
     *
787
     * When strict mode is enabled, if you are running multiple groups of
788
     * transactions, if one group fails all subsequent groups will be
789
     * rolled back.
790
     *
791
     * If strict mode is disabled, each group is treated autonomously,
792
     * meaning a failure of one group will not affect any others
793
     *
794
     * @param bool $mode = true
795
     *
796
     * @return $this
797
     */
798
    public function transStrict(bool $mode = true)
799
    {
800
        $this->transStrict = $mode;
4✔
801

802
        return $this;
4✔
803
    }
804

805
    /**
806
     * Start Transaction
807
     */
808
    public function transStart(bool $testMode = false): bool
809
    {
810
        if (! $this->transEnabled) {
49✔
811
            return false;
×
812
        }
813

814
        return $this->transBegin($testMode);
49✔
815
    }
816

817
    /**
818
     * If set to true, exceptions are thrown during transactions.
819
     *
820
     * @return $this
821
     */
822
    public function transException(bool $transException)
823
    {
824
        $this->transException = $transException;
3✔
825

826
        return $this;
3✔
827
    }
828

829
    /**
830
     * Complete Transaction
831
     */
832
    public function transComplete(): bool
833
    {
834
        if (! $this->transEnabled) {
49✔
835
            return false;
×
836
        }
837

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

842
            // If we are NOT running in strict mode, we will reset
843
            // the _trans_status flag so that subsequent groups of
844
            // transactions will be permitted.
845
            if ($this->transStrict === false) {
16✔
846
                $this->transStatus = true;
4✔
847
            }
848

849
            return false;
16✔
850
        }
851

852
        return $this->transCommit();
38✔
853
    }
854

855
    /**
856
     * Lets you retrieve the transaction flag to determine if it has failed
857
     */
858
    public function transStatus(): bool
859
    {
860
        return $this->transStatus;
15✔
861
    }
862

863
    /**
864
     * Begin Transaction
865
     */
866
    public function transBegin(bool $testMode = false): bool
867
    {
868
        if (! $this->transEnabled) {
52✔
869
            return false;
×
870
        }
871

872
        // When transactions are nested we only begin/commit/rollback the outermost ones
873
        if ($this->transDepth > 0) {
52✔
874
            $this->transDepth++;
×
875

876
            return true;
×
877
        }
878

879
        if (empty($this->connID)) {
52✔
880
            $this->initialize();
×
881
        }
882

883
        // Reset the transaction failure flag.
884
        // If the $testMode flag is set to TRUE transactions will be rolled back
885
        // even if the queries produce a successful result.
886
        $this->transFailure = $testMode;
52✔
887

888
        if ($this->_transBegin()) {
52✔
889
            $this->transDepth++;
52✔
890

891
            return true;
52✔
892
        }
893

894
        return false;
×
895
    }
896

897
    /**
898
     * Commit Transaction
899
     */
900
    public function transCommit(): bool
901
    {
902
        if (! $this->transEnabled || $this->transDepth === 0) {
38✔
903
            return false;
×
904
        }
905

906
        // When transactions are nested we only begin/commit/rollback the outermost ones
907
        if ($this->transDepth > 1 || $this->_transCommit()) {
38✔
908
            $this->transDepth--;
38✔
909

910
            return true;
38✔
911
        }
912

913
        return false;
×
914
    }
915

916
    /**
917
     * Rollback Transaction
918
     */
919
    public function transRollback(): bool
920
    {
921
        if (! $this->transEnabled || $this->transDepth === 0) {
19✔
922
            return false;
×
923
        }
924

925
        // When transactions are nested we only begin/commit/rollback the outermost ones
926
        if ($this->transDepth > 1 || $this->_transRollback()) {
19✔
927
            $this->transDepth--;
19✔
928

929
            return true;
19✔
930
        }
931

932
        return false;
×
933
    }
934

935
    /**
936
     * Reset transaction status - to restart transactions after strict mode failure
937
     */
938
    public function resetTransStatus(): static
939
    {
940
        $this->transStatus = true;
4✔
941

942
        return $this;
4✔
943
    }
944

945
    /**
946
     * Handle transaction status when a query fails
947
     *
948
     * @internal This method is for internal database component use only
949
     */
950
    public function handleTransStatus(): void
951
    {
952
        if ($this->transDepth !== 0) {
37✔
953
            $this->transStatus = false;
18✔
954
        }
955
    }
956

957
    /**
958
     * Begin Transaction
959
     */
960
    abstract protected function _transBegin(): bool;
961

962
    /**
963
     * Commit Transaction
964
     */
965
    abstract protected function _transCommit(): bool;
966

967
    /**
968
     * Rollback Transaction
969
     */
970
    abstract protected function _transRollback(): bool;
971

972
    /**
973
     * Returns a non-shared new instance of the query builder for this connection.
974
     *
975
     * @param array|string|TableName $tableName
976
     *
977
     * @return BaseBuilder
978
     *
979
     * @throws DatabaseException
980
     */
981
    public function table($tableName)
982
    {
983
        if (empty($tableName)) {
928✔
984
            throw new DatabaseException('You must set the database table to be used with your query.');
×
985
        }
986

987
        $className = str_replace('Connection', 'Builder', static::class);
928✔
988

989
        return new $className($tableName, $this);
928✔
990
    }
991

992
    /**
993
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
994
     */
995
    public function newQuery(): BaseBuilder
996
    {
997
        // save table aliases
998
        $tempAliases         = $this->aliasedTables;
14✔
999
        $builder             = $this->table(',')->from([], true);
14✔
1000
        $this->aliasedTables = $tempAliases;
14✔
1001

1002
        return $builder;
14✔
1003
    }
1004

1005
    /**
1006
     * Creates a prepared statement with the database that can then
1007
     * be used to execute multiple statements against. Within the
1008
     * closure, you would build the query in any normal way, though
1009
     * the Query Builder is the expected manner.
1010
     *
1011
     * Example:
1012
     *    $stmt = $db->prepare(function($db)
1013
     *           {
1014
     *             return $db->table('users')
1015
     *                   ->where('id', 1)
1016
     *                     ->get();
1017
     *           })
1018
     *
1019
     * @param Closure(BaseConnection): mixed $func
1020
     *
1021
     * @return BasePreparedQuery|null
1022
     */
1023
    public function prepare(Closure $func, array $options = [])
1024
    {
1025
        if (empty($this->connID)) {
14✔
1026
            $this->initialize();
×
1027
        }
1028

1029
        $this->pretend();
14✔
1030

1031
        $sql = $func($this);
14✔
1032

1033
        $this->pretend(false);
14✔
1034

1035
        if ($sql instanceof QueryInterface) {
14✔
1036
            $sql = $sql->getOriginalQuery();
14✔
1037
        }
1038

1039
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
14✔
1040
        /** @var BasePreparedQuery $class */
1041
        $class = new $class($this);
14✔
1042

1043
        return $class->prepare($sql, $options);
14✔
1044
    }
1045

1046
    /**
1047
     * Returns the last query's statement object.
1048
     *
1049
     * @return Query
1050
     */
1051
    public function getLastQuery()
1052
    {
1053
        return $this->lastQuery;
11✔
1054
    }
1055

1056
    /**
1057
     * Returns a string representation of the last query's statement object.
1058
     */
1059
    public function showLastQuery(): string
1060
    {
1061
        return (string) $this->lastQuery;
×
1062
    }
1063

1064
    /**
1065
     * Returns the time we started to connect to this database in
1066
     * seconds with microseconds.
1067
     *
1068
     * Used by the Debug Toolbar's timeline.
1069
     */
1070
    public function getConnectStart(): ?float
1071
    {
1072
        return $this->connectTime;
1✔
1073
    }
1074

1075
    /**
1076
     * Returns the number of seconds with microseconds that it took
1077
     * to connect to the database.
1078
     *
1079
     * Used by the Debug Toolbar's timeline.
1080
     */
1081
    public function getConnectDuration(int $decimals = 6): string
1082
    {
1083
        return number_format($this->connectDuration, $decimals);
2✔
1084
    }
1085

1086
    /**
1087
     * Protect Identifiers
1088
     *
1089
     * This function is used extensively by the Query Builder class, and by
1090
     * a couple functions in this class.
1091
     * It takes a column or table name (optionally with an alias) and inserts
1092
     * the table prefix onto it. Some logic is necessary in order to deal with
1093
     * column names that include the path. Consider a query like this:
1094
     *
1095
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1096
     *
1097
     * Or a query with aliasing:
1098
     *
1099
     * SELECT m.member_id, m.member_name FROM members AS m
1100
     *
1101
     * Since the column name can include up to four segments (host, DB, table, column)
1102
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1103
     * insert the table prefix (if it exists) in the proper position, and escape only
1104
     * the correct identifiers.
1105
     *
1106
     * @param array|int|string|TableName $item
1107
     * @param bool                       $prefixSingle       Prefix a table name with no segments?
1108
     * @param bool                       $protectIdentifiers Protect table or column names?
1109
     * @param bool                       $fieldExists        Supplied $item contains a column name?
1110
     *
1111
     * @return ($item is array ? array : string)
1112
     */
1113
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1114
    {
1115
        if (! is_bool($protectIdentifiers)) {
1,091✔
1116
            $protectIdentifiers = $this->protectIdentifiers;
1,058✔
1117
        }
1118

1119
        if (is_array($item)) {
1,091✔
1120
            $escapedArray = [];
1✔
1121

1122
            foreach ($item as $k => $v) {
1✔
1123
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1124
            }
1125

1126
            return $escapedArray;
1✔
1127
        }
1128

1129
        if ($item instanceof TableName) {
1,091✔
1130
            /** @psalm-suppress NoValue I don't know why ERROR. */
1131
            return $this->escapeTableName($item);
2✔
1132
        }
1133

1134
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1135
        $item = (string) $item;
1,091✔
1136

1137
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1138
        // If a parenthesis is found we know that we do not need to
1139
        // escape the data or add a prefix. There's probably a more graceful
1140
        // way to deal with this, but I'm not thinking of it
1141
        //
1142
        // Added exception for single quotes as well, we don't want to alter
1143
        // literal strings.
1144
        if (strcspn($item, "()'") !== strlen($item)) {
1,091✔
1145
            /** @psalm-suppress NoValue I don't know why ERROR. */
1146
            return $item;
779✔
1147
        }
1148

1149
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1150
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,080✔
1151
            /** @psalm-suppress NoValue I don't know why ERROR. */
1152
            return $item;
100✔
1153
        }
1154

1155
        // Convert tabs or multiple spaces into single spaces
1156
        /** @psalm-suppress NoValue I don't know why ERROR. */
1157
        $item = preg_replace('/\s+/', ' ', trim($item));
1,079✔
1158

1159
        // If the item has an alias declaration we remove it and set it aside.
1160
        // Note: strripos() is used in order to support spaces in table names
1161
        if ($offset = strripos($item, ' AS ')) {
1,079✔
1162
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1163
            $item  = substr($item, 0, $offset);
11✔
1164
        } elseif ($offset = strrpos($item, ' ')) {
1,074✔
1165
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
12✔
1166
            $item  = substr($item, 0, $offset);
12✔
1167
        } else {
1168
            $alias = '';
1,068✔
1169
        }
1170

1171
        // Break the string apart if it contains periods, then insert the table prefix
1172
        // in the correct location, assuming the period doesn't indicate that we're dealing
1173
        // with an alias. While we're at it, we will escape the components
1174
        if (str_contains($item, '.')) {
1,079✔
1175
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
135✔
1176
        }
1177

1178
        // In some cases, especially 'from', we end up running through
1179
        // protect_identifiers twice. This algorithm won't work when
1180
        // it contains the escapeChar so strip it out.
1181
        $item = trim($item, $this->escapeChar);
1,071✔
1182

1183
        // Is there a table prefix? If not, no need to insert it
1184
        if ($this->DBPrefix !== '') {
1,071✔
1185
            // Verify table prefix and replace if necessary
1186
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
819✔
1187
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1188
            }
1189
            // Do we prefix an item with no segments?
1190
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
819✔
1191
                $item = $this->DBPrefix . $item;
812✔
1192
            }
1193
        }
1194

1195
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,071✔
1196
            $item = $this->escapeIdentifiers($item);
1,069✔
1197
        }
1198

1199
        return $item . $alias;
1,071✔
1200
    }
1201

1202
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1203
    {
1204
        $parts = explode('.', $item);
135✔
1205

1206
        // Does the first segment of the exploded item match
1207
        // one of the aliases previously identified? If so,
1208
        // we have nothing more to do other than escape the item
1209
        //
1210
        // NOTE: The ! empty() condition prevents this method
1211
        // from breaking when QB isn't enabled.
1212
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
135✔
1213
            if ($protectIdentifiers) {
10✔
1214
                foreach ($parts as $key => $val) {
10✔
1215
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10✔
1216
                        $parts[$key] = $this->escapeIdentifiers($val);
10✔
1217
                    }
1218
                }
1219

1220
                $item = implode('.', $parts);
10✔
1221
            }
1222

1223
            return $item . $alias;
10✔
1224
        }
1225

1226
        // Is there a table prefix defined in the config file? If not, no need to do anything
1227
        if ($this->DBPrefix !== '') {
129✔
1228
            // We now add the table prefix based on some logic.
1229
            // Do we have 4 segments (hostname.database.table.column)?
1230
            // If so, we add the table prefix to the column name in the 3rd segment.
1231
            if (isset($parts[3])) {
121✔
1232
                $i = 2;
×
1233
            }
1234
            // Do we have 3 segments (database.table.column)?
1235
            // If so, we add the table prefix to the column name in 2nd position
1236
            elseif (isset($parts[2])) {
121✔
1237
                $i = 1;
×
1238
            }
1239
            // Do we have 2 segments (table.column)?
1240
            // If so, we add the table prefix to the column name in 1st segment
1241
            else {
1242
                $i = 0;
121✔
1243
            }
1244

1245
            // This flag is set when the supplied $item does not contain a field name.
1246
            // This can happen when this function is being called from a JOIN.
1247
            if ($fieldExists === false) {
121✔
1248
                $i++;
×
1249
            }
1250

1251
            // Verify table prefix and replace if necessary
1252
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
121✔
1253
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1254
            }
1255
            // We only add the table prefix if it does not already exist
1256
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
121✔
1257
                $parts[$i] = $this->DBPrefix . $parts[$i];
121✔
1258
            }
1259

1260
            // Put the parts back together
1261
            $item = implode('.', $parts);
121✔
1262
        }
1263

1264
        if ($protectIdentifiers) {
129✔
1265
            $item = $this->escapeIdentifiers($item);
129✔
1266
        }
1267

1268
        return $item . $alias;
129✔
1269
    }
1270

1271
    /**
1272
     * Escape the SQL Identifier
1273
     *
1274
     * This function escapes single identifier.
1275
     *
1276
     * @param non-empty-string|TableName $item
1277
     */
1278
    public function escapeIdentifier($item): string
1279
    {
1280
        if ($item === '') {
712✔
1281
            return '';
×
1282
        }
1283

1284
        if ($item instanceof TableName) {
712✔
1285
            return $this->escapeTableName($item);
7✔
1286
        }
1287

1288
        return $this->escapeChar
712✔
1289
            . str_replace(
712✔
1290
                $this->escapeChar,
712✔
1291
                $this->escapeChar . $this->escapeChar,
712✔
1292
                $item,
712✔
1293
            )
712✔
1294
            . $this->escapeChar;
712✔
1295
    }
1296

1297
    /**
1298
     * Returns escaped table name with alias.
1299
     */
1300
    private function escapeTableName(TableName $tableName): string
1301
    {
1302
        $alias = $tableName->getAlias();
7✔
1303

1304
        return $this->escapeIdentifier($tableName->getActualTableName())
7✔
1305
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7✔
1306
    }
1307

1308
    /**
1309
     * Escape the SQL Identifiers
1310
     *
1311
     * This function escapes column and table names
1312
     *
1313
     * @param array|string $item
1314
     *
1315
     * @return ($item is array ? array : string)
1316
     */
1317
    public function escapeIdentifiers($item)
1318
    {
1319
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
1,097✔
1320
            return $item;
5✔
1321
        }
1322

1323
        if (is_array($item)) {
1,096✔
1324
            foreach ($item as $key => $value) {
724✔
1325
                $item[$key] = $this->escapeIdentifiers($value);
724✔
1326
            }
1327

1328
            return $item;
724✔
1329
        }
1330

1331
        // Avoid breaking functions and literal values inside queries
1332
        if (ctype_digit($item)
1,096✔
1333
            || $item[0] === "'"
1,095✔
1334
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,095✔
1335
            || str_contains($item, '(')) {
1,096✔
1336
            return $item;
45✔
1337
        }
1338

1339
        if ($this->pregEscapeChar === []) {
1,095✔
1340
            if (is_array($this->escapeChar)) {
302✔
1341
                $this->pregEscapeChar = [
×
1342
                    preg_quote($this->escapeChar[0], '/'),
×
1343
                    preg_quote($this->escapeChar[1], '/'),
×
1344
                    $this->escapeChar[0],
×
1345
                    $this->escapeChar[1],
×
1346
                ];
×
1347
            } else {
1348
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
302✔
1349
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
302✔
1350
            }
1351
        }
1352

1353
        foreach ($this->reservedIdentifiers as $id) {
1,095✔
1354
            /** @psalm-suppress NoValue I don't know why ERROR. */
1355
            if (str_contains($item, '.' . $id)) {
1,095✔
1356
                return preg_replace(
3✔
1357
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1358
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1359
                    $item,
3✔
1360
                );
3✔
1361
            }
1362
        }
1363

1364
        /** @psalm-suppress NoValue I don't know why ERROR. */
1365
        return preg_replace(
1,093✔
1366
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,093✔
1367
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,093✔
1368
            $item,
1,093✔
1369
        );
1,093✔
1370
    }
1371

1372
    /**
1373
     * Prepends a database prefix if one exists in configuration
1374
     *
1375
     * @throws DatabaseException
1376
     */
1377
    public function prefixTable(string $table = ''): string
1378
    {
1379
        if ($table === '') {
3✔
1380
            throw new DatabaseException('A table name is required for that operation.');
×
1381
        }
1382

1383
        return $this->DBPrefix . $table;
3✔
1384
    }
1385

1386
    /**
1387
     * Returns the total number of rows affected by this query.
1388
     */
1389
    abstract public function affectedRows(): int;
1390

1391
    /**
1392
     * "Smart" Escape String
1393
     *
1394
     * Escapes data based on type.
1395
     * Sets boolean and null types
1396
     *
1397
     * @param array|bool|float|int|object|string|null $str
1398
     *
1399
     * @return ($str is array ? array : float|int|string)
1400
     */
1401
    public function escape($str)
1402
    {
1403
        if (is_array($str)) {
913✔
1404
            return array_map($this->escape(...), $str);
741✔
1405
        }
1406

1407
        if ($str instanceof Stringable) {
913✔
1408
            if ($str instanceof RawSql) {
15✔
1409
                return $str->__toString();
14✔
1410
            }
1411

1412
            $str = (string) $str;
1✔
1413
        }
1414

1415
        if (is_string($str)) {
910✔
1416
            return "'" . $this->escapeString($str) . "'";
860✔
1417
        }
1418

1419
        if (is_bool($str)) {
831✔
1420
            return ($str === false) ? 0 : 1;
8✔
1421
        }
1422

1423
        return $str ?? 'NULL';
829✔
1424
    }
1425

1426
    /**
1427
     * Escape String
1428
     *
1429
     * @param list<string|Stringable>|string|Stringable $str  Input string
1430
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1431
     *
1432
     * @return list<string>|string
1433
     */
1434
    public function escapeString($str, bool $like = false)
1435
    {
1436
        if (is_array($str)) {
860✔
1437
            foreach ($str as $key => $val) {
×
1438
                $str[$key] = $this->escapeString($val, $like);
×
1439
            }
1440

1441
            return $str;
×
1442
        }
1443

1444
        if ($str instanceof Stringable) {
860✔
1445
            if ($str instanceof RawSql) {
2✔
1446
                return $str->__toString();
×
1447
            }
1448

1449
            $str = (string) $str;
2✔
1450
        }
1451

1452
        $str = $this->_escapeString($str);
860✔
1453

1454
        // escape LIKE condition wildcards
1455
        if ($like) {
860✔
1456
            return str_replace(
2✔
1457
                [
2✔
1458
                    $this->likeEscapeChar,
2✔
1459
                    '%',
2✔
1460
                    '_',
2✔
1461
                ],
2✔
1462
                [
2✔
1463
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1464
                    $this->likeEscapeChar . '%',
2✔
1465
                    $this->likeEscapeChar . '_',
2✔
1466
                ],
2✔
1467
                $str,
2✔
1468
            );
2✔
1469
        }
1470

1471
        return $str;
860✔
1472
    }
1473

1474
    /**
1475
     * Escape LIKE String
1476
     *
1477
     * Calls the individual driver for platform
1478
     * specific escaping for LIKE conditions
1479
     *
1480
     * @param list<string|Stringable>|string|Stringable $str
1481
     *
1482
     * @return list<string>|string
1483
     */
1484
    public function escapeLikeString($str)
1485
    {
1486
        return $this->escapeString($str, true);
2✔
1487
    }
1488

1489
    /**
1490
     * Platform independent string escape.
1491
     *
1492
     * Will likely be overridden in child classes.
1493
     */
1494
    protected function _escapeString(string $str): string
1495
    {
1496
        return str_replace("'", "''", remove_invisible_characters($str, false));
814✔
1497
    }
1498

1499
    /**
1500
     * This function enables you to call PHP database functions that are not natively included
1501
     * in CodeIgniter, in a platform independent manner.
1502
     *
1503
     * @param array ...$params
1504
     *
1505
     * @throws DatabaseException
1506
     */
1507
    public function callFunction(string $functionName, ...$params): bool
1508
    {
1509
        $driver = $this->getDriverFunctionPrefix();
×
1510

1511
        if (! str_contains($driver, $functionName)) {
×
1512
            $functionName = $driver . $functionName;
×
1513
        }
1514

1515
        if (! function_exists($functionName)) {
×
1516
            if ($this->DBDebug) {
×
1517
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1518
            }
1519

1520
            return false;
×
1521
        }
1522

1523
        return $functionName(...$params);
×
1524
    }
1525

1526
    /**
1527
     * Get the prefix of the function to access the DB.
1528
     */
1529
    protected function getDriverFunctionPrefix(): string
1530
    {
1531
        return strtolower($this->DBDriver) . '_';
×
1532
    }
1533

1534
    // --------------------------------------------------------------------
1535
    // META Methods
1536
    // --------------------------------------------------------------------
1537

1538
    /**
1539
     * Returns an array of table names
1540
     *
1541
     * @return false|list<string>
1542
     *
1543
     * @throws DatabaseException
1544
     */
1545
    public function listTables(bool $constrainByPrefix = false)
1546
    {
1547
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
772✔
1548
            return $constrainByPrefix
766✔
1549
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1550
                : $this->dataCache['table_names'];
766✔
1551
        }
1552

1553
        $sql = $this->_listTables($constrainByPrefix);
57✔
1554

1555
        if ($sql === false) {
57✔
1556
            if ($this->DBDebug) {
×
1557
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1558
            }
1559

1560
            return false;
×
1561
        }
1562

1563
        $this->dataCache['table_names'] = [];
57✔
1564

1565
        $query = $this->query($sql);
57✔
1566

1567
        foreach ($query->getResultArray() as $row) {
57✔
1568
            /** @var string $table */
1569
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
54✔
1570

1571
            $this->dataCache['table_names'][] = $table;
54✔
1572
        }
1573

1574
        return $this->dataCache['table_names'];
57✔
1575
    }
1576

1577
    /**
1578
     * Determine if a particular table exists
1579
     *
1580
     * @param bool $cached Whether to use data cache
1581
     */
1582
    public function tableExists(string $tableName, bool $cached = true): bool
1583
    {
1584
        if ($cached) {
766✔
1585
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
765✔
1586
        }
1587

1588
        if (false === ($sql = $this->_listTables(false, $tableName))) {
719✔
1589
            if ($this->DBDebug) {
×
1590
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1591
            }
1592

1593
            return false;
×
1594
        }
1595

1596
        $tableExists = $this->query($sql)->getResultArray() !== [];
719✔
1597

1598
        // if cache has been built already
1599
        if (! empty($this->dataCache['table_names'])) {
719✔
1600
            $key = array_search(
715✔
1601
                strtolower($tableName),
715✔
1602
                array_map(strtolower(...), $this->dataCache['table_names']),
715✔
1603
                true,
715✔
1604
            );
715✔
1605

1606
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1607
            // OR if table does exist but is not found in cache
1608
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
715✔
1609
                $this->resetDataCache();
1✔
1610
            }
1611
        }
1612

1613
        return $tableExists;
719✔
1614
    }
1615

1616
    /**
1617
     * Fetch Field Names
1618
     *
1619
     * @param string|TableName $tableName
1620
     *
1621
     * @return false|list<string>
1622
     *
1623
     * @throws DatabaseException
1624
     */
1625
    public function getFieldNames($tableName)
1626
    {
1627
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12✔
1628

1629
        // Is there a cached result?
1630
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1631
            return $this->dataCache['field_names'][$table];
7✔
1632
        }
1633

1634
        if (empty($this->connID)) {
8✔
1635
            $this->initialize();
×
1636
        }
1637

1638
        if (false === ($sql = $this->_listColumns($tableName))) {
8✔
1639
            if ($this->DBDebug) {
×
1640
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1641
            }
1642

1643
            return false;
×
1644
        }
1645

1646
        $query = $this->query($sql);
8✔
1647

1648
        $this->dataCache['field_names'][$table] = [];
8✔
1649

1650
        foreach ($query->getResultArray() as $row) {
8✔
1651
            // Do we know from where to get the column's name?
1652
            if (! isset($key)) {
8✔
1653
                if (isset($row['column_name'])) {
8✔
1654
                    $key = 'column_name';
8✔
1655
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1656
                    $key = 'COLUMN_NAME';
8✔
1657
                } else {
1658
                    // We have no other choice but to just get the first element's key.
1659
                    $key = key($row);
8✔
1660
                }
1661
            }
1662

1663
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1664
        }
1665

1666
        return $this->dataCache['field_names'][$table];
8✔
1667
    }
1668

1669
    /**
1670
     * Determine if a particular field exists
1671
     */
1672
    public function fieldExists(string $fieldName, string $tableName): bool
1673
    {
1674
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1675
    }
1676

1677
    /**
1678
     * Returns an object with field data
1679
     *
1680
     * @return list<stdClass>
1681
     */
1682
    public function getFieldData(string $table)
1683
    {
1684
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
144✔
1685
    }
1686

1687
    /**
1688
     * Returns an object with key data
1689
     *
1690
     * @return array<string, stdClass>
1691
     */
1692
    public function getIndexData(string $table)
1693
    {
1694
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
160✔
1695
    }
1696

1697
    /**
1698
     * Returns an object with foreign key data
1699
     *
1700
     * @return array<string, stdClass>
1701
     */
1702
    public function getForeignKeyData(string $table)
1703
    {
1704
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
37✔
1705
    }
1706

1707
    /**
1708
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1709
     *
1710
     * @return array<string, stdClass>
1711
     *
1712
     * array[
1713
     *    {constraint_name} =>
1714
     *        stdClass[
1715
     *            'constraint_name'     => string,
1716
     *            'table_name'          => string,
1717
     *            'column_name'         => string[],
1718
     *            'foreign_table_name'  => string,
1719
     *            'foreign_column_name' => string[],
1720
     *            'on_delete'           => string,
1721
     *            'on_update'           => string,
1722
     *            'match'               => string
1723
     *        ]
1724
     * ]
1725
     */
1726
    protected function foreignKeyDataToObjects(array $data)
1727
    {
1728
        $retVal = [];
37✔
1729

1730
        foreach ($data as $row) {
37✔
1731
            $name = $row['constraint_name'];
12✔
1732

1733
            // for sqlite generate name
1734
            if ($name === null) {
12✔
1735
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1736
            }
1737

1738
            $obj                      = new stdClass();
12✔
1739
            $obj->constraint_name     = $name;
12✔
1740
            $obj->table_name          = $row['table_name'];
12✔
1741
            $obj->column_name         = $row['column_name'];
12✔
1742
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1743
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1744
            $obj->on_delete           = $row['on_delete'];
12✔
1745
            $obj->on_update           = $row['on_update'];
12✔
1746
            $obj->match               = $row['match'];
12✔
1747

1748
            $retVal[$name] = $obj;
12✔
1749
        }
1750

1751
        return $retVal;
37✔
1752
    }
1753

1754
    /**
1755
     * Disables foreign key checks temporarily.
1756
     *
1757
     * @return bool
1758
     */
1759
    public function disableForeignKeyChecks()
1760
    {
1761
        $sql = $this->_disableForeignKeyChecks();
733✔
1762

1763
        if ($sql === '') {
733✔
1764
            // The feature is not supported.
1765
            return false;
×
1766
        }
1767

1768
        return $this->query($sql);
733✔
1769
    }
1770

1771
    /**
1772
     * Enables foreign key checks temporarily.
1773
     *
1774
     * @return bool
1775
     */
1776
    public function enableForeignKeyChecks()
1777
    {
1778
        $sql = $this->_enableForeignKeyChecks();
816✔
1779

1780
        if ($sql === '') {
816✔
1781
            // The feature is not supported.
1782
            return false;
×
1783
        }
1784

1785
        return $this->query($sql);
816✔
1786
    }
1787

1788
    /**
1789
     * Allows the engine to be set into a mode where queries are not
1790
     * actually executed, but they are still generated, timed, etc.
1791
     *
1792
     * This is primarily used by the prepared query functionality.
1793
     *
1794
     * @return $this
1795
     */
1796
    public function pretend(bool $pretend = true)
1797
    {
1798
        $this->pretend = $pretend;
15✔
1799

1800
        return $this;
15✔
1801
    }
1802

1803
    /**
1804
     * Empties our data cache. Especially helpful during testing.
1805
     *
1806
     * @return $this
1807
     */
1808
    public function resetDataCache()
1809
    {
1810
        $this->dataCache = [];
36✔
1811

1812
        return $this;
36✔
1813
    }
1814

1815
    /**
1816
     * Determines if the statement is a write-type query or not.
1817
     *
1818
     * @param string $sql
1819
     */
1820
    public function isWriteType($sql): bool
1821
    {
1822
        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);
842✔
1823
    }
1824

1825
    /**
1826
     * Returns the last error code and message.
1827
     *
1828
     * Must return an array with keys 'code' and 'message':
1829
     *
1830
     * @return array{code: int|string|null, message: string|null}
1831
     */
1832
    abstract public function error(): array;
1833

1834
    /**
1835
     * Insert ID
1836
     *
1837
     * @return int|string
1838
     */
1839
    abstract public function insertID();
1840

1841
    /**
1842
     * Generates the SQL for listing tables in a platform-dependent manner.
1843
     *
1844
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1845
     *
1846
     * @return false|string
1847
     */
1848
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1849

1850
    /**
1851
     * Generates a platform-specific query string so that the column names can be fetched.
1852
     *
1853
     * @param string|TableName $table
1854
     *
1855
     * @return false|string
1856
     */
1857
    abstract protected function _listColumns($table = '');
1858

1859
    /**
1860
     * Platform-specific field data information.
1861
     *
1862
     * @see getFieldData()
1863
     *
1864
     * @return list<stdClass>
1865
     */
1866
    abstract protected function _fieldData(string $table): array;
1867

1868
    /**
1869
     * Platform-specific index data.
1870
     *
1871
     * @see    getIndexData()
1872
     *
1873
     * @return array<string, stdClass>
1874
     */
1875
    abstract protected function _indexData(string $table): array;
1876

1877
    /**
1878
     * Platform-specific foreign keys data.
1879
     *
1880
     * @see    getForeignKeyData()
1881
     *
1882
     * @return array<string, stdClass>
1883
     */
1884
    abstract protected function _foreignKeyData(string $table): array;
1885

1886
    /**
1887
     * Platform-specific SQL statement to disable foreign key checks.
1888
     *
1889
     * If this feature is not supported, return empty string.
1890
     *
1891
     * @TODO This method should be moved to an interface that represents foreign key support.
1892
     *
1893
     * @return string
1894
     *
1895
     * @see disableForeignKeyChecks()
1896
     */
1897
    protected function _disableForeignKeyChecks()
1898
    {
1899
        return '';
×
1900
    }
1901

1902
    /**
1903
     * Platform-specific SQL statement to enable foreign key checks.
1904
     *
1905
     * If this feature is not supported, return empty string.
1906
     *
1907
     * @TODO This method should be moved to an interface that represents foreign key support.
1908
     *
1909
     * @return string
1910
     *
1911
     * @see enableForeignKeyChecks()
1912
     */
1913
    protected function _enableForeignKeyChecks()
1914
    {
1915
        return '';
×
1916
    }
1917

1918
    /**
1919
     * Accessor for properties if they exist.
1920
     *
1921
     * @return array|bool|float|int|object|resource|string|null
1922
     */
1923
    public function __get(string $key)
1924
    {
1925
        if (property_exists($this, $key)) {
1,064✔
1926
            return $this->{$key};
1,063✔
1927
        }
1928

1929
        return null;
1✔
1930
    }
1931

1932
    /**
1933
     * Checker for properties existence.
1934
     */
1935
    public function __isset(string $key): bool
1936
    {
1937
        return property_exists($this, $key);
245✔
1938
    }
1939
}
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