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

codeigniter4 / CodeIgniter4 / 22495985083

27 Feb 2026 05:08PM UTC coverage: 86.622% (+0.03%) from 86.594%
22495985083

push

github

web-flow
refactor: remove deprecations in `Database` (#9986)

7 of 9 new or added lines in 2 files covered. (77.78%)

283 existing lines in 13 files now uncovered.

22513 of 25990 relevant lines covered (86.62%)

218.55 hits per line

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

87.44
/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 CodeIgniter\I18n\Time;
20
use Exception;
21
use stdClass;
22
use Stringable;
23
use Throwable;
24

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

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

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

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

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

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

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

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

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

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

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

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

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

160
    /**
161
     * Database session timezone
162
     *
163
     * false    = Don't set timezone (default, backward compatible)
164
     * true     = Automatically sync with app timezone
165
     * string   = Specific timezone (offset or named timezone)
166
     *
167
     * Named timezones (e.g., 'America/New_York') will be automatically
168
     * converted to offsets (e.g., '-05:00') for database compatibility.
169
     *
170
     * @var bool|string
171
     */
172
    protected $timezone = false;
173

174
    /**
175
     * Swap Prefix
176
     *
177
     * @var string
178
     */
179
    protected $swapPre = '';
180

181
    /**
182
     * Encryption flag/data
183
     *
184
     * @var array|bool
185
     */
186
    protected $encrypt = false;
187

188
    /**
189
     * Compression flag
190
     *
191
     * @var bool
192
     */
193
    protected $compress = false;
194

195
    /**
196
     * Settings for a failover connection.
197
     *
198
     * @var array
199
     */
200
    protected $failover = [];
201

202
    /**
203
     * The last query object that was executed
204
     * on this connection.
205
     *
206
     * @var Query
207
     */
208
    protected $lastQuery;
209

210
    /**
211
     * The exception that would have been thrown on the last failed query
212
     * if DBDebug were enabled. Null when the last query succeeded or when
213
     * DBDebug is true (in which case the exception is thrown directly and
214
     * this property is never set).
215
     */
216
    protected ?DatabaseException $lastException = null;
217

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

387
        foreach ($params as $key => $value) {
441✔
388
            if (property_exists($this, $key)) {
147✔
389
                $this->{$key} = $value;
147✔
390
            }
391
        }
392

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

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

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

407
    /**
408
     * Initializes the database connection/settings.
409
     *
410
     * @return void
411
     *
412
     * @throws DatabaseException
413
     */
414
    public function initialize()
415
    {
416
        /* If an established connection is available, then there's
417
         * no need to connect and select the database.
418
         *
419
         * Depending on the database driver, connID can be either
420
         * boolean TRUE, a resource or an object.
421
         */
422
        if ($this->connID) {
836✔
423
            return;
800✔
424
        }
425

426
        $this->connectTime = microtime(true);
52✔
427
        $connectionErrors  = [];
52✔
428

429
        try {
430
            // Connect to the database and set the connection ID
431
            $this->connID = $this->connect($this->pConnect);
52✔
432
        } catch (Throwable $e) {
2✔
433
            $this->connID       = false;
2✔
434
            $connectionErrors[] = sprintf(
2✔
435
                'Main connection [%s]: %s',
2✔
436
                $this->DBDriver,
2✔
437
                $e->getMessage(),
2✔
438
            );
2✔
439
            log_message('error', 'Error connecting to the database: ' . $e);
2✔
440
        }
441

442
        // No connection resource? Check if there is a failover else throw an error
443
        if (! $this->connID) {
52✔
444
            // Check if there is a failover set
445
            if (! empty($this->failover) && is_array($this->failover)) {
4✔
446
                // Go over all the failovers
447
                foreach ($this->failover as $index => $failover) {
2✔
448
                    // Replace the current settings with those of the failover
449
                    foreach ($failover as $key => $val) {
2✔
450
                        if (property_exists($this, $key)) {
2✔
451
                            $this->{$key} = $val;
2✔
452
                        }
453
                    }
454

455
                    try {
456
                        // Try to connect
457
                        $this->connID = $this->connect($this->pConnect);
2✔
458
                    } catch (Throwable $e) {
1✔
459
                        $connectionErrors[] = sprintf(
1✔
460
                            'Failover #%d [%s]: %s',
1✔
461
                            ++$index,
1✔
462
                            $this->DBDriver,
1✔
463
                            $e->getMessage(),
1✔
464
                        );
1✔
465
                        log_message('error', 'Error connecting to the database: ' . $e);
1✔
466
                    }
467

468
                    // If a connection is made break the foreach loop
469
                    if ($this->connID) {
2✔
470
                        break;
2✔
471
                    }
472
                }
473
            }
474

475
            // We still don't have a connection?
476
            if (! $this->connID) {
4✔
477
                throw new DatabaseException(sprintf(
2✔
478
                    'Unable to connect to the database.%s%s',
2✔
479
                    PHP_EOL,
2✔
480
                    implode(PHP_EOL, $connectionErrors),
2✔
481
                ));
2✔
482
            }
483
        }
484

485
        $this->connectDuration = microtime(true) - $this->connectTime;
50✔
486
    }
487

488
    /**
489
     * Close the database connection.
490
     *
491
     * @return void
492
     */
493
    public function close()
494
    {
495
        if ($this->connID) {
3✔
496
            $this->_close();
3✔
497
            $this->connID = false;
3✔
498
        }
499
    }
500

501
    /**
502
     * Keep or establish the connection if no queries have been sent for
503
     * a length of time exceeding the server's idle timeout.
504
     *
505
     * @return void
506
     */
507
    public function reconnect()
508
    {
509
        if ($this->ping() === false) {
2✔
510
            $this->close();
1✔
511
            $this->initialize();
1✔
512
        }
513
    }
514

515
    /**
516
     * Platform dependent way method for closing the connection.
517
     *
518
     * @return void
519
     */
520
    abstract protected function _close();
521

522
    /**
523
     * Check if the connection is still alive.
524
     */
525
    public function ping(): bool
526
    {
527
        if ($this->connID === false) {
5✔
528
            return false;
2✔
529
        }
530

531
        return $this->_ping();
4✔
532
    }
533

534
    /**
535
     * Driver-specific ping implementation.
536
     */
537
    protected function _ping(): bool
538
    {
539
        try {
540
            $result = $this->simpleQuery('SELECT 1');
4✔
541

542
            return $result !== false;
4✔
UNCOV
543
        } catch (DatabaseException) {
×
UNCOV
544
            return false;
×
545
        }
546
    }
547

548
    /**
549
     * Create a persistent database connection.
550
     *
551
     * @return false|TConnection
552
     */
553
    public function persistentConnect()
554
    {
UNCOV
555
        return $this->connect(true);
×
556
    }
557

558
    /**
559
     * Returns the actual connection object. If both a 'read' and 'write'
560
     * connection has been specified, you can pass either term in to
561
     * get that connection. If you pass either alias in and only a single
562
     * connection is present, it must return the sole connection.
563
     *
564
     * @return false|TConnection
565
     */
566
    public function getConnection(?string $alias = null)
567
    {
568
        // @todo work with read/write connections
569
        return $this->connID;
2✔
570
    }
571

572
    /**
573
     * Returns the name of the current database being used.
574
     */
575
    public function getDatabase(): string
576
    {
577
        return empty($this->database) ? '' : $this->database;
780✔
578
    }
579

580
    /**
581
     * Set DB Prefix
582
     *
583
     * Set's the DB Prefix to something new without needing to reconnect
584
     *
585
     * @param string $prefix The prefix
586
     */
587
    public function setPrefix(string $prefix = ''): string
588
    {
589
        return $this->DBPrefix = $prefix;
13✔
590
    }
591

592
    /**
593
     * Returns the database prefix.
594
     */
595
    public function getPrefix(): string
596
    {
597
        return $this->DBPrefix;
12✔
598
    }
599

600
    /**
601
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
602
     */
603
    public function getPlatform(): string
604
    {
605
        return $this->DBDriver;
23✔
606
    }
607

608
    /**
609
     * Sets the Table Aliases to use. These are typically
610
     * collected during use of the Builder, and set here
611
     * so queries are built correctly.
612
     *
613
     * @return $this
614
     */
615
    public function setAliasedTables(array $aliases)
616
    {
617
        $this->aliasedTables = $aliases;
1,003✔
618

619
        return $this;
1,003✔
620
    }
621

622
    /**
623
     * Add a table alias to our list.
624
     *
625
     * @return $this
626
     */
627
    public function addTableAlias(string $alias)
628
    {
629
        if ($alias === '') {
30✔
630
            return $this;
6✔
631
        }
632

633
        if (! in_array($alias, $this->aliasedTables, true)) {
24✔
634
            $this->aliasedTables[] = $alias;
24✔
635
        }
636

637
        return $this;
24✔
638
    }
639

640
    /**
641
     * Executes the query against the database.
642
     *
643
     * @return false|TResult
644
     */
645
    abstract protected function execute(string $sql);
646

647
    /**
648
     * Orchestrates a query against the database. Queries must use
649
     * Database\Statement objects to store the query and build it.
650
     * This method works with the cache.
651
     *
652
     * Should automatically handle different connections for read/write
653
     * queries if needed.
654
     *
655
     * @param array|string|null $binds
656
     *
657
     * @return BaseResult<TConnection, TResult>|bool|Query
658
     *
659
     * @todo BC set $queryClass default as null in 4.1
660
     */
661
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
662
    {
663
        $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass;
835✔
664

665
        if (empty($this->connID)) {
835✔
666
            $this->initialize();
27✔
667
        }
668

669
        /**
670
         * @var Query $query
671
         */
672
        $query = new $queryClass($this);
835✔
673

674
        $query->setQuery($sql, $binds, $setEscapeFlags);
835✔
675

676
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
835✔
UNCOV
677
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
678
        }
679

680
        $startTime = microtime(true);
835✔
681

682
        // Always save the last query so we can use
683
        // the getLastQuery() method.
684
        $this->lastQuery = $query;
835✔
685

686
        // If $pretend is true, then we just want to return
687
        // the actual query object here. There won't be
688
        // any results to return.
689
        if ($this->pretend) {
835✔
690
            $query->setDuration($startTime);
10✔
691

692
            return $query;
10✔
693
        }
694

695
        // Run the query for real
696
        try {
697
            $exception           = null;
835✔
698
            $this->lastException = null;
835✔
699
            $this->resultID      = $this->simpleQuery($query->getQuery());
835✔
700
        } catch (DatabaseException $exception) {
16✔
701
            $this->resultID = false;
16✔
702
        }
703

704
        if ($this->resultID === false) {
835✔
705
            $query->setDuration($startTime, $startTime);
36✔
706

707
            // This will trigger a rollback if transactions are being used
708
            $this->handleTransStatus();
36✔
709

710
            if (
711
                $this->DBDebug
36✔
712
                && (
713
                    // Not in transactions
714
                    $this->transDepth === 0
36✔
715
                    // In transactions, do not throw exception by default.
36✔
716
                    || $this->transException
36✔
717
                )
718
            ) {
719
                // We call this function in order to roll-back queries
720
                // if transactions are enabled. If we don't call this here
721
                // the error message will trigger an exit, causing the
722
                // transactions to remain in limbo.
723
                while ($this->transDepth !== 0) {
12✔
724
                    $transDepth = $this->transDepth;
2✔
725
                    $this->transComplete();
2✔
726

727
                    if ($transDepth === $this->transDepth) {
2✔
UNCOV
728
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
UNCOV
729
                        break;
×
730
                    }
731
                }
732

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

736
                if ($exception instanceof DatabaseException) {
12✔
737
                    throw $exception;
10✔
738
                }
739

740
                return false;
2✔
741
            }
742

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

746
            return false;
24✔
747
        }
748

749
        $query->setDuration($startTime);
835✔
750

751
        // Let others do something with this query
752
        Events::trigger('DBQuery', $query);
835✔
753

754
        // resultID is not false, so it must be successful
755
        if ($this->isWriteType($sql)) {
835✔
756
            return true;
796✔
757
        }
758

759
        // query is not write-type, so it must be read-type query; return QueryResult
760
        $resultClass = str_replace('Connection', 'Result', static::class);
834✔
761

762
        return new $resultClass($this->connID, $this->resultID);
834✔
763
    }
764

765
    /**
766
     * Performs a basic query against the database. No binding or caching
767
     * is performed, nor are transactions handled. Simply takes a raw
768
     * query string and returns the database-specific result id.
769
     *
770
     * @return false|TResult
771
     */
772
    public function simpleQuery(string $sql)
773
    {
774
        if (empty($this->connID)) {
842✔
775
            $this->initialize();
6✔
776
        }
777

778
        return $this->execute($sql);
842✔
779
    }
780

781
    /**
782
     * Disable Transactions
783
     *
784
     * This permits transactions to be disabled at run-time.
785
     *
786
     * @return void
787
     */
788
    public function transOff()
789
    {
UNCOV
790
        $this->transEnabled = false;
×
791
    }
792

793
    /**
794
     * Enable/disable Transaction Strict Mode
795
     *
796
     * When strict mode is enabled, if you are running multiple groups of
797
     * transactions, if one group fails all subsequent groups will be
798
     * rolled back.
799
     *
800
     * If strict mode is disabled, each group is treated autonomously,
801
     * meaning a failure of one group will not affect any others
802
     *
803
     * @param bool $mode = true
804
     *
805
     * @return $this
806
     */
807
    public function transStrict(bool $mode = true)
808
    {
809
        $this->transStrict = $mode;
4✔
810

811
        return $this;
4✔
812
    }
813

814
    /**
815
     * Start Transaction
816
     */
817
    public function transStart(bool $testMode = false): bool
818
    {
819
        if (! $this->transEnabled) {
49✔
UNCOV
820
            return false;
×
821
        }
822

823
        return $this->transBegin($testMode);
49✔
824
    }
825

826
    /**
827
     * If set to true, exceptions are thrown during transactions.
828
     *
829
     * @return $this
830
     */
831
    public function transException(bool $transException)
832
    {
833
        $this->transException = $transException;
3✔
834

835
        return $this;
3✔
836
    }
837

838
    /**
839
     * Complete Transaction
840
     */
841
    public function transComplete(): bool
842
    {
843
        if (! $this->transEnabled) {
49✔
UNCOV
844
            return false;
×
845
        }
846

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

851
            // If we are NOT running in strict mode, we will reset
852
            // the _trans_status flag so that subsequent groups of
853
            // transactions will be permitted.
854
            if ($this->transStrict === false) {
16✔
855
                $this->transStatus = true;
4✔
856
            }
857

858
            return false;
16✔
859
        }
860

861
        return $this->transCommit();
38✔
862
    }
863

864
    /**
865
     * Lets you retrieve the transaction flag to determine if it has failed
866
     */
867
    public function transStatus(): bool
868
    {
869
        return $this->transStatus;
15✔
870
    }
871

872
    /**
873
     * Begin Transaction
874
     */
875
    public function transBegin(bool $testMode = false): bool
876
    {
877
        if (! $this->transEnabled) {
52✔
878
            return false;
×
879
        }
880

881
        // When transactions are nested we only begin/commit/rollback the outermost ones
882
        if ($this->transDepth > 0) {
52✔
UNCOV
883
            $this->transDepth++;
×
884

UNCOV
885
            return true;
×
886
        }
887

888
        if (empty($this->connID)) {
52✔
UNCOV
889
            $this->initialize();
×
890
        }
891

892
        // Reset the transaction failure flag.
893
        // If the $testMode flag is set to TRUE transactions will be rolled back
894
        // even if the queries produce a successful result.
895
        $this->transFailure = $testMode;
52✔
896

897
        if ($this->_transBegin()) {
52✔
898
            $this->transDepth++;
52✔
899

900
            return true;
52✔
901
        }
902

UNCOV
903
        return false;
×
904
    }
905

906
    /**
907
     * Commit Transaction
908
     */
909
    public function transCommit(): bool
910
    {
911
        if (! $this->transEnabled || $this->transDepth === 0) {
38✔
UNCOV
912
            return false;
×
913
        }
914

915
        // When transactions are nested we only begin/commit/rollback the outermost ones
916
        if ($this->transDepth > 1 || $this->_transCommit()) {
38✔
917
            $this->transDepth--;
38✔
918

919
            return true;
38✔
920
        }
921

UNCOV
922
        return false;
×
923
    }
924

925
    /**
926
     * Rollback Transaction
927
     */
928
    public function transRollback(): bool
929
    {
930
        if (! $this->transEnabled || $this->transDepth === 0) {
19✔
UNCOV
931
            return false;
×
932
        }
933

934
        // When transactions are nested we only begin/commit/rollback the outermost ones
935
        if ($this->transDepth > 1 || $this->_transRollback()) {
19✔
936
            $this->transDepth--;
19✔
937

938
            return true;
19✔
939
        }
940

UNCOV
941
        return false;
×
942
    }
943

944
    /**
945
     * Reset transaction status - to restart transactions after strict mode failure
946
     */
947
    public function resetTransStatus(): static
948
    {
949
        $this->transStatus = true;
4✔
950

951
        return $this;
4✔
952
    }
953

954
    /**
955
     * Handle transaction status when a query fails
956
     *
957
     * @internal This method is for internal database component use only
958
     */
959
    public function handleTransStatus(): void
960
    {
961
        if ($this->transDepth !== 0) {
40✔
962
            $this->transStatus = false;
18✔
963
        }
964
    }
965

966
    /**
967
     * Begin Transaction
968
     */
969
    abstract protected function _transBegin(): bool;
970

971
    /**
972
     * Commit Transaction
973
     */
974
    abstract protected function _transCommit(): bool;
975

976
    /**
977
     * Rollback Transaction
978
     */
979
    abstract protected function _transRollback(): bool;
980

981
    /**
982
     * Returns a non-shared new instance of the query builder for this connection.
983
     *
984
     * @param array|string|TableName $tableName
985
     *
986
     * @return BaseBuilder
987
     *
988
     * @throws DatabaseException
989
     */
990
    public function table($tableName)
991
    {
992
        if (empty($tableName)) {
945✔
UNCOV
993
            throw new DatabaseException('You must set the database table to be used with your query.');
×
994
        }
995

996
        $className = str_replace('Connection', 'Builder', static::class);
945✔
997

998
        return new $className($tableName, $this);
945✔
999
    }
1000

1001
    /**
1002
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
1003
     */
1004
    public function newQuery(): BaseBuilder
1005
    {
1006
        // save table aliases
1007
        $tempAliases         = $this->aliasedTables;
14✔
1008
        $builder             = $this->table(',')->from([], true);
14✔
1009
        $this->aliasedTables = $tempAliases;
14✔
1010

1011
        return $builder;
14✔
1012
    }
1013

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

1038
        $this->pretend();
16✔
1039

1040
        $sql = $func($this);
16✔
1041

1042
        $this->pretend(false);
16✔
1043

1044
        if ($sql instanceof QueryInterface) {
16✔
1045
            $sql = $sql->getOriginalQuery();
16✔
1046
        }
1047

1048
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
16✔
1049
        /** @var BasePreparedQuery $class */
1050
        $class = new $class($this);
16✔
1051

1052
        return $class->prepare($sql, $options);
16✔
1053
    }
1054

1055
    /**
1056
     * Returns the last query's statement object.
1057
     *
1058
     * @return Query
1059
     */
1060
    public function getLastQuery()
1061
    {
1062
        return $this->lastQuery;
11✔
1063
    }
1064

1065
    /**
1066
     * Returns a string representation of the last query's statement object.
1067
     */
1068
    public function showLastQuery(): string
1069
    {
UNCOV
1070
        return (string) $this->lastQuery;
×
1071
    }
1072

1073
    /**
1074
     * Returns the time we started to connect to this database in
1075
     * seconds with microseconds.
1076
     *
1077
     * Used by the Debug Toolbar's timeline.
1078
     */
1079
    public function getConnectStart(): ?float
1080
    {
1081
        return $this->connectTime;
1✔
1082
    }
1083

1084
    /**
1085
     * Returns the number of seconds with microseconds that it took
1086
     * to connect to the database.
1087
     *
1088
     * Used by the Debug Toolbar's timeline.
1089
     */
1090
    public function getConnectDuration(int $decimals = 6): string
1091
    {
1092
        return number_format($this->connectDuration, $decimals);
2✔
1093
    }
1094

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

1128
        if (is_array($item)) {
1,108✔
1129
            $escapedArray = [];
1✔
1130

1131
            foreach ($item as $k => $v) {
1✔
1132
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1133
            }
1134

1135
            return $escapedArray;
1✔
1136
        }
1137

1138
        if ($item instanceof TableName) {
1,108✔
1139
            /** @psalm-suppress NoValue I don't know why ERROR. */
1140
            return $this->escapeTableName($item);
2✔
1141
        }
1142

1143
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1144
        $item = (string) $item;
1,108✔
1145

1146
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1147
        // If a parenthesis is found we know that we do not need to
1148
        // escape the data or add a prefix. There's probably a more graceful
1149
        // way to deal with this, but I'm not thinking of it
1150
        //
1151
        // Added exception for single quotes as well, we don't want to alter
1152
        // literal strings.
1153
        if (strcspn($item, "()'") !== strlen($item)) {
1,108✔
1154
            /** @psalm-suppress NoValue I don't know why ERROR. */
1155
            return $item;
796✔
1156
        }
1157

1158
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1159
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,097✔
1160
            /** @psalm-suppress NoValue I don't know why ERROR. */
1161
            return $item;
100✔
1162
        }
1163

1164
        // Convert tabs or multiple spaces into single spaces
1165
        /** @psalm-suppress NoValue I don't know why ERROR. */
1166
        $item = preg_replace('/\s+/', ' ', trim($item));
1,096✔
1167

1168
        // If the item has an alias declaration we remove it and set it aside.
1169
        // Note: strripos() is used in order to support spaces in table names
1170
        if ($offset = strripos($item, ' AS ')) {
1,096✔
1171
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1172
            $item  = substr($item, 0, $offset);
11✔
1173
        } elseif ($offset = strrpos($item, ' ')) {
1,091✔
1174
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
12✔
1175
            $item  = substr($item, 0, $offset);
12✔
1176
        } else {
1177
            $alias = '';
1,085✔
1178
        }
1179

1180
        // Break the string apart if it contains periods, then insert the table prefix
1181
        // in the correct location, assuming the period doesn't indicate that we're dealing
1182
        // with an alias. While we're at it, we will escape the components
1183
        if (str_contains($item, '.')) {
1,096✔
1184
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
135✔
1185
        }
1186

1187
        // In some cases, especially 'from', we end up running through
1188
        // protect_identifiers twice. This algorithm won't work when
1189
        // it contains the escapeChar so strip it out.
1190
        $item = trim($item, $this->escapeChar);
1,088✔
1191

1192
        // Is there a table prefix? If not, no need to insert it
1193
        if ($this->DBPrefix !== '') {
1,088✔
1194
            // Verify table prefix and replace if necessary
1195
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
836✔
UNCOV
1196
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1197
            }
1198
            // Do we prefix an item with no segments?
1199
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
836✔
1200
                $item = $this->DBPrefix . $item;
829✔
1201
            }
1202
        }
1203

1204
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,088✔
1205
            $item = $this->escapeIdentifiers($item);
1,086✔
1206
        }
1207

1208
        return $item . $alias;
1,088✔
1209
    }
1210

1211
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1212
    {
1213
        $parts = explode('.', $item);
135✔
1214

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

1229
                $item = implode('.', $parts);
10✔
1230
            }
1231

1232
            return $item . $alias;
10✔
1233
        }
1234

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

1254
            // This flag is set when the supplied $item does not contain a field name.
1255
            // This can happen when this function is being called from a JOIN.
1256
            if ($fieldExists === false) {
121✔
1257
                $i++;
×
1258
            }
1259

1260
            // Verify table prefix and replace if necessary
1261
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
121✔
UNCOV
1262
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1263
            }
1264
            // We only add the table prefix if it does not already exist
1265
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
121✔
1266
                $parts[$i] = $this->DBPrefix . $parts[$i];
121✔
1267
            }
1268

1269
            // Put the parts back together
1270
            $item = implode('.', $parts);
121✔
1271
        }
1272

1273
        if ($protectIdentifiers) {
129✔
1274
            $item = $this->escapeIdentifiers($item);
129✔
1275
        }
1276

1277
        return $item . $alias;
129✔
1278
    }
1279

1280
    /**
1281
     * Escape the SQL Identifier
1282
     *
1283
     * This function escapes single identifier.
1284
     *
1285
     * @param non-empty-string|TableName $item
1286
     */
1287
    public function escapeIdentifier($item): string
1288
    {
1289
        if ($item === '') {
728✔
UNCOV
1290
            return '';
×
1291
        }
1292

1293
        if ($item instanceof TableName) {
728✔
1294
            return $this->escapeTableName($item);
7✔
1295
        }
1296

1297
        return $this->escapeChar
728✔
1298
            . str_replace(
728✔
1299
                $this->escapeChar,
728✔
1300
                $this->escapeChar . $this->escapeChar,
728✔
1301
                $item,
728✔
1302
            )
728✔
1303
            . $this->escapeChar;
728✔
1304
    }
1305

1306
    /**
1307
     * Returns escaped table name with alias.
1308
     */
1309
    private function escapeTableName(TableName $tableName): string
1310
    {
1311
        $alias = $tableName->getAlias();
7✔
1312

1313
        return $this->escapeIdentifier($tableName->getActualTableName())
7✔
1314
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7✔
1315
    }
1316

1317
    /**
1318
     * Escape the SQL Identifiers
1319
     *
1320
     * This function escapes column and table names
1321
     *
1322
     * @param array|string $item
1323
     *
1324
     * @return ($item is array ? array : string)
1325
     */
1326
    public function escapeIdentifiers($item)
1327
    {
1328
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
1,114✔
1329
            return $item;
5✔
1330
        }
1331

1332
        if (is_array($item)) {
1,113✔
1333
            foreach ($item as $key => $value) {
741✔
1334
                $item[$key] = $this->escapeIdentifiers($value);
741✔
1335
            }
1336

1337
            return $item;
741✔
1338
        }
1339

1340
        // Avoid breaking functions and literal values inside queries
1341
        if (ctype_digit($item)
1,113✔
1342
            || $item[0] === "'"
1,112✔
1343
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,112✔
1344
            || str_contains($item, '(')) {
1,113✔
1345
            return $item;
47✔
1346
        }
1347

1348
        if ($this->pregEscapeChar === []) {
1,112✔
1349
            if (is_array($this->escapeChar)) {
301✔
1350
                $this->pregEscapeChar = [
×
UNCOV
1351
                    preg_quote($this->escapeChar[0], '/'),
×
UNCOV
1352
                    preg_quote($this->escapeChar[1], '/'),
×
UNCOV
1353
                    $this->escapeChar[0],
×
UNCOV
1354
                    $this->escapeChar[1],
×
UNCOV
1355
                ];
×
1356
            } else {
1357
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
301✔
1358
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
301✔
1359
            }
1360
        }
1361

1362
        foreach ($this->reservedIdentifiers as $id) {
1,112✔
1363
            /** @psalm-suppress NoValue I don't know why ERROR. */
1364
            if (str_contains($item, '.' . $id)) {
1,112✔
1365
                return preg_replace(
3✔
1366
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1367
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1368
                    $item,
3✔
1369
                );
3✔
1370
            }
1371
        }
1372

1373
        /** @psalm-suppress NoValue I don't know why ERROR. */
1374
        return preg_replace(
1,110✔
1375
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,110✔
1376
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,110✔
1377
            $item,
1,110✔
1378
        );
1,110✔
1379
    }
1380

1381
    /**
1382
     * Prepends a database prefix if one exists in configuration
1383
     *
1384
     * @throws DatabaseException
1385
     */
1386
    public function prefixTable(string $table = ''): string
1387
    {
1388
        if ($table === '') {
3✔
UNCOV
1389
            throw new DatabaseException('A table name is required for that operation.');
×
1390
        }
1391

1392
        return $this->DBPrefix . $table;
3✔
1393
    }
1394

1395
    /**
1396
     * Returns the total number of rows affected by this query.
1397
     */
1398
    abstract public function affectedRows(): int;
1399

1400
    /**
1401
     * "Smart" Escape String
1402
     *
1403
     * Escapes data based on type.
1404
     * Sets boolean and null types
1405
     *
1406
     * @param array|bool|float|int|object|string|null $str
1407
     *
1408
     * @return ($str is array ? array : float|int|string)
1409
     */
1410
    public function escape($str)
1411
    {
1412
        if (is_array($str)) {
930✔
1413
            return array_map($this->escape(...), $str);
757✔
1414
        }
1415

1416
        if ($str instanceof Stringable) {
930✔
1417
            if ($str instanceof RawSql) {
13✔
1418
                return $str->__toString();
12✔
1419
            }
1420

1421
            $str = (string) $str;
1✔
1422
        }
1423

1424
        if (is_string($str)) {
927✔
1425
            return "'" . $this->escapeString($str) . "'";
876✔
1426
        }
1427

1428
        if (is_bool($str)) {
848✔
1429
            return ($str === false) ? 0 : 1;
8✔
1430
        }
1431

1432
        return $str ?? 'NULL';
846✔
1433
    }
1434

1435
    /**
1436
     * Escape String
1437
     *
1438
     * @param list<string|Stringable>|string|Stringable $str  Input string
1439
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1440
     *
1441
     * @return list<string>|string
1442
     */
1443
    public function escapeString($str, bool $like = false)
1444
    {
1445
        if (is_array($str)) {
876✔
UNCOV
1446
            foreach ($str as $key => $val) {
×
UNCOV
1447
                $str[$key] = $this->escapeString($val, $like);
×
1448
            }
1449

1450
            return $str;
×
1451
        }
1452

1453
        if ($str instanceof Stringable) {
876✔
1454
            if ($str instanceof RawSql) {
2✔
UNCOV
1455
                return $str->__toString();
×
1456
            }
1457

1458
            $str = (string) $str;
2✔
1459
        }
1460

1461
        $str = $this->_escapeString($str);
876✔
1462

1463
        // escape LIKE condition wildcards
1464
        if ($like) {
876✔
1465
            return str_replace(
2✔
1466
                [
2✔
1467
                    $this->likeEscapeChar,
2✔
1468
                    '%',
2✔
1469
                    '_',
2✔
1470
                ],
2✔
1471
                [
2✔
1472
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1473
                    $this->likeEscapeChar . '%',
2✔
1474
                    $this->likeEscapeChar . '_',
2✔
1475
                ],
2✔
1476
                $str,
2✔
1477
            );
2✔
1478
        }
1479

1480
        return $str;
876✔
1481
    }
1482

1483
    /**
1484
     * Escape LIKE String
1485
     *
1486
     * Calls the individual driver for platform
1487
     * specific escaping for LIKE conditions
1488
     *
1489
     * @param list<string|Stringable>|string|Stringable $str
1490
     *
1491
     * @return list<string>|string
1492
     */
1493
    public function escapeLikeString($str)
1494
    {
1495
        return $this->escapeString($str, true);
2✔
1496
    }
1497

1498
    /**
1499
     * Platform independent string escape.
1500
     *
1501
     * Will likely be overridden in child classes.
1502
     */
1503
    protected function _escapeString(string $str): string
1504
    {
1505
        return str_replace("'", "''", remove_invisible_characters($str, false));
832✔
1506
    }
1507

1508
    /**
1509
     * This function enables you to call PHP database functions that are not natively included
1510
     * in CodeIgniter, in a platform independent manner.
1511
     *
1512
     * @param array ...$params
1513
     *
1514
     * @throws DatabaseException
1515
     */
1516
    public function callFunction(string $functionName, ...$params): bool
1517
    {
1518
        $driver = $this->getDriverFunctionPrefix();
2✔
1519

1520
        if (! str_starts_with($functionName, $driver)) {
2✔
1521
            $functionName = $driver . $functionName;
1✔
1522
        }
1523

1524
        if (! function_exists($functionName)) {
2✔
UNCOV
1525
            if ($this->DBDebug) {
×
UNCOV
1526
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1527
            }
1528

UNCOV
1529
            return false;
×
1530
        }
1531

1532
        return $functionName(...$params);
2✔
1533
    }
1534

1535
    /**
1536
     * Get the prefix of the function to access the DB.
1537
     */
1538
    protected function getDriverFunctionPrefix(): string
1539
    {
UNCOV
1540
        return strtolower($this->DBDriver) . '_';
×
1541
    }
1542

1543
    // --------------------------------------------------------------------
1544
    // META Methods
1545
    // --------------------------------------------------------------------
1546

1547
    /**
1548
     * Returns an array of table names
1549
     *
1550
     * @return false|list<string>
1551
     *
1552
     * @throws DatabaseException
1553
     */
1554
    public function listTables(bool $constrainByPrefix = false)
1555
    {
1556
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
789✔
1557
            return $constrainByPrefix
783✔
1558
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1559
                : $this->dataCache['table_names'];
783✔
1560
        }
1561

1562
        $sql = $this->_listTables($constrainByPrefix);
57✔
1563

1564
        if ($sql === false) {
57✔
UNCOV
1565
            if ($this->DBDebug) {
×
UNCOV
1566
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1567
            }
1568

UNCOV
1569
            return false;
×
1570
        }
1571

1572
        $this->dataCache['table_names'] = [];
57✔
1573

1574
        $query = $this->query($sql);
57✔
1575

1576
        foreach ($query->getResultArray() as $row) {
57✔
1577
            /** @var string $table */
1578
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
54✔
1579

1580
            $this->dataCache['table_names'][] = $table;
54✔
1581
        }
1582

1583
        return $this->dataCache['table_names'];
57✔
1584
    }
1585

1586
    /**
1587
     * Determine if a particular table exists
1588
     *
1589
     * @param bool $cached Whether to use data cache
1590
     */
1591
    public function tableExists(string $tableName, bool $cached = true): bool
1592
    {
1593
        if ($cached) {
783✔
1594
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
782✔
1595
        }
1596

1597
        if (false === ($sql = $this->_listTables(false, $tableName))) {
736✔
UNCOV
1598
            if ($this->DBDebug) {
×
UNCOV
1599
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1600
            }
1601

UNCOV
1602
            return false;
×
1603
        }
1604

1605
        $tableExists = $this->query($sql)->getResultArray() !== [];
736✔
1606

1607
        // if cache has been built already
1608
        if (! empty($this->dataCache['table_names'])) {
736✔
1609
            $key = array_search(
732✔
1610
                strtolower($tableName),
732✔
1611
                array_map(strtolower(...), $this->dataCache['table_names']),
732✔
1612
                true,
732✔
1613
            );
732✔
1614

1615
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1616
            // OR if table does exist but is not found in cache
1617
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
732✔
1618
                $this->resetDataCache();
1✔
1619
            }
1620
        }
1621

1622
        return $tableExists;
736✔
1623
    }
1624

1625
    /**
1626
     * Fetch Field Names
1627
     *
1628
     * @param string|TableName $tableName
1629
     *
1630
     * @return false|list<string>
1631
     *
1632
     * @throws DatabaseException
1633
     */
1634
    public function getFieldNames($tableName)
1635
    {
1636
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12✔
1637

1638
        // Is there a cached result?
1639
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1640
            return $this->dataCache['field_names'][$table];
7✔
1641
        }
1642

1643
        if (empty($this->connID)) {
8✔
1644
            $this->initialize();
×
1645
        }
1646

1647
        if (false === ($sql = $this->_listColumns($tableName))) {
8✔
UNCOV
1648
            if ($this->DBDebug) {
×
UNCOV
1649
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1650
            }
1651

UNCOV
1652
            return false;
×
1653
        }
1654

1655
        $query = $this->query($sql);
8✔
1656

1657
        $this->dataCache['field_names'][$table] = [];
8✔
1658

1659
        foreach ($query->getResultArray() as $row) {
8✔
1660
            // Do we know from where to get the column's name?
1661
            if (! isset($key)) {
8✔
1662
                if (isset($row['column_name'])) {
8✔
1663
                    $key = 'column_name';
8✔
1664
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1665
                    $key = 'COLUMN_NAME';
8✔
1666
                } else {
1667
                    // We have no other choice but to just get the first element's key.
1668
                    $key = key($row);
8✔
1669
                }
1670
            }
1671

1672
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1673
        }
1674

1675
        return $this->dataCache['field_names'][$table];
8✔
1676
    }
1677

1678
    /**
1679
     * Determine if a particular field exists
1680
     */
1681
    public function fieldExists(string $fieldName, string $tableName): bool
1682
    {
1683
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1684
    }
1685

1686
    /**
1687
     * Returns an object with field data
1688
     *
1689
     * @return list<stdClass>
1690
     */
1691
    public function getFieldData(string $table)
1692
    {
1693
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
145✔
1694
    }
1695

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

1706
    /**
1707
     * Returns an object with foreign key data
1708
     *
1709
     * @return array<string, stdClass>
1710
     */
1711
    public function getForeignKeyData(string $table)
1712
    {
1713
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
37✔
1714
    }
1715

1716
    /**
1717
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1718
     *
1719
     * @return array<string, stdClass>
1720
     *
1721
     * array[
1722
     *    {constraint_name} =>
1723
     *        stdClass[
1724
     *            'constraint_name'     => string,
1725
     *            'table_name'          => string,
1726
     *            'column_name'         => string[],
1727
     *            'foreign_table_name'  => string,
1728
     *            'foreign_column_name' => string[],
1729
     *            'on_delete'           => string,
1730
     *            'on_update'           => string,
1731
     *            'match'               => string
1732
     *        ]
1733
     * ]
1734
     */
1735
    protected function foreignKeyDataToObjects(array $data)
1736
    {
1737
        $retVal = [];
37✔
1738

1739
        foreach ($data as $row) {
37✔
1740
            $name = $row['constraint_name'];
12✔
1741

1742
            // for sqlite generate name
1743
            if ($name === null) {
12✔
1744
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1745
            }
1746

1747
            $obj                      = new stdClass();
12✔
1748
            $obj->constraint_name     = $name;
12✔
1749
            $obj->table_name          = $row['table_name'];
12✔
1750
            $obj->column_name         = $row['column_name'];
12✔
1751
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1752
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1753
            $obj->on_delete           = $row['on_delete'];
12✔
1754
            $obj->on_update           = $row['on_update'];
12✔
1755
            $obj->match               = $row['match'];
12✔
1756

1757
            $retVal[$name] = $obj;
12✔
1758
        }
1759

1760
        return $retVal;
37✔
1761
    }
1762

1763
    /**
1764
     * Disables foreign key checks temporarily.
1765
     *
1766
     * @return bool
1767
     */
1768
    public function disableForeignKeyChecks()
1769
    {
1770
        $sql = $this->_disableForeignKeyChecks();
750✔
1771

1772
        if ($sql === '') {
750✔
1773
            // The feature is not supported.
UNCOV
1774
            return false;
×
1775
        }
1776

1777
        return $this->query($sql);
750✔
1778
    }
1779

1780
    /**
1781
     * Enables foreign key checks temporarily.
1782
     *
1783
     * @return bool
1784
     */
1785
    public function enableForeignKeyChecks()
1786
    {
1787
        $sql = $this->_enableForeignKeyChecks();
833✔
1788

1789
        if ($sql === '') {
833✔
1790
            // The feature is not supported.
UNCOV
1791
            return false;
×
1792
        }
1793

1794
        return $this->query($sql);
833✔
1795
    }
1796

1797
    /**
1798
     * Allows the engine to be set into a mode where queries are not
1799
     * actually executed, but they are still generated, timed, etc.
1800
     *
1801
     * This is primarily used by the prepared query functionality.
1802
     *
1803
     * @return $this
1804
     */
1805
    public function pretend(bool $pretend = true)
1806
    {
1807
        $this->pretend = $pretend;
17✔
1808

1809
        return $this;
17✔
1810
    }
1811

1812
    /**
1813
     * Empties our data cache. Especially helpful during testing.
1814
     *
1815
     * @return $this
1816
     */
1817
    public function resetDataCache()
1818
    {
1819
        $this->dataCache = [];
36✔
1820

1821
        return $this;
36✔
1822
    }
1823

1824
    /**
1825
     * Determines if the statement is a write-type query or not.
1826
     *
1827
     * @param string $sql
1828
     */
1829
    public function isWriteType($sql): bool
1830
    {
1831
        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);
859✔
1832
    }
1833

1834
    /**
1835
     * Returns the last error code and message.
1836
     *
1837
     * Must return an array with keys 'code' and 'message':
1838
     *
1839
     * @return array{code: int|string|null, message: string|null}
1840
     */
1841
    abstract public function error(): array;
1842

1843
    /**
1844
     * Returns the exception that would have been thrown on the last failed
1845
     * query if DBDebug were enabled. Returns null if the last query succeeded
1846
     * or if DBDebug is true (in which case the exception is always thrown
1847
     * directly and this method will always return null).
1848
     */
1849
    public function getLastException(): ?DatabaseException
1850
    {
1851
        return $this->lastException;
1✔
1852
    }
1853

1854
    /**
1855
     * Insert ID
1856
     *
1857
     * @return int|string
1858
     */
1859
    abstract public function insertID();
1860

1861
    /**
1862
     * Generates the SQL for listing tables in a platform-dependent manner.
1863
     *
1864
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1865
     *
1866
     * @return false|string
1867
     */
1868
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1869

1870
    /**
1871
     * Generates a platform-specific query string so that the column names can be fetched.
1872
     *
1873
     * @param string|TableName $table
1874
     *
1875
     * @return false|string
1876
     */
1877
    abstract protected function _listColumns($table = '');
1878

1879
    /**
1880
     * Platform-specific field data information.
1881
     *
1882
     * @see getFieldData()
1883
     *
1884
     * @return list<stdClass>
1885
     */
1886
    abstract protected function _fieldData(string $table): array;
1887

1888
    /**
1889
     * Platform-specific index data.
1890
     *
1891
     * @see    getIndexData()
1892
     *
1893
     * @return array<string, stdClass>
1894
     */
1895
    abstract protected function _indexData(string $table): array;
1896

1897
    /**
1898
     * Platform-specific foreign keys data.
1899
     *
1900
     * @see    getForeignKeyData()
1901
     *
1902
     * @return array<string, stdClass>
1903
     */
1904
    abstract protected function _foreignKeyData(string $table): array;
1905

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

1922
    /**
1923
     * Platform-specific SQL statement to enable foreign key checks.
1924
     *
1925
     * If this feature is not supported, return empty string.
1926
     *
1927
     * @TODO This method should be moved to an interface that represents foreign key support.
1928
     *
1929
     * @return string
1930
     *
1931
     * @see enableForeignKeyChecks()
1932
     */
1933
    protected function _enableForeignKeyChecks()
1934
    {
UNCOV
1935
        return '';
×
1936
    }
1937

1938
    /**
1939
     * Converts a named timezone to an offset string.
1940
     *
1941
     * Converts timezone identifiers (e.g., 'America/New_York') to offset strings
1942
     * (e.g., '-05:00' or '-04:00' depending on DST). This is useful because not all
1943
     * databases have timezone tables loaded, but all support offset notation.
1944
     *
1945
     * @param string $timezone Named timezone (e.g., 'America/New_York', 'UTC', 'Europe/Paris')
1946
     *
1947
     * @return string Offset string (e.g., '+00:00', '-05:00', '+01:00')
1948
     */
1949
    protected function convertTimezoneToOffset(string $timezone): string
1950
    {
1951
        // If it's already an offset, return as-is
1952
        if (preg_match('/^[+-]\d{2}:\d{2}$/', $timezone)) {
9✔
1953
            return $timezone;
3✔
1954
        }
1955

1956
        try {
1957
            $offset = Time::now($timezone)->getOffset();
6✔
1958

1959
            // Convert offset seconds to +-HH:MM format
1960
            $hours   = (int) ($offset / 3600);
5✔
1961
            $minutes = abs((int) (($offset % 3600) / 60));
5✔
1962

1963
            return sprintf('%+03d:%02d', $hours, $minutes);
5✔
1964
        } catch (Exception $e) {
1✔
1965
            // If timezone conversion fails, log and return UTC
1966
            log_message('error', "Invalid timezone '{$timezone}'. Falling back to UTC. {$e->getMessage()}.");
1✔
1967

1968
            return '+00:00';
1✔
1969
        }
1970
    }
1971

1972
    /**
1973
     * Gets the timezone string to use for database session.
1974
     *
1975
     * Handles the timezone configuration logic:
1976
     * - false: Don't set timezone (returns null)
1977
     * - true: Auto-sync with app timezone from config
1978
     * - string: Use specific timezone (converts named timezones to offsets)
1979
     *
1980
     * @return string|null The timezone offset string, or null if timezone should not be set
1981
     */
1982
    protected function getSessionTimezone(): ?string
1983
    {
1984
        if ($this->timezone === false) {
41✔
1985
            return null;
35✔
1986
        }
1987

1988
        // Auto-sync with app timezone
1989
        if ($this->timezone === true) {
6✔
1990
            $appConfig = config('App');
2✔
1991
            $timezone  = $appConfig->appTimezone ?? 'UTC';
2✔
1992
        } else {
1993
            // Use specific timezone from config
1994
            $timezone = $this->timezone;
4✔
1995
        }
1996

1997
        return $this->convertTimezoneToOffset($timezone);
6✔
1998
    }
1999

2000
    /**
2001
     * Accessor for properties if they exist.
2002
     *
2003
     * @return array|bool|float|int|object|resource|string|null
2004
     */
2005
    public function __get(string $key)
2006
    {
2007
        if (property_exists($this, $key)) {
1,084✔
2008
            return $this->{$key};
1,083✔
2009
        }
2010

2011
        return null;
1✔
2012
    }
2013

2014
    /**
2015
     * Checker for properties existence.
2016
     */
2017
    public function __isset(string $key): bool
2018
    {
2019
        return property_exists($this, $key);
250✔
2020
    }
2021
}
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