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

codeigniter4 / CodeIgniter4 / 9366456936

04 Jun 2024 11:44AM UTC coverage: 84.448% (+0.006%) from 84.442%
9366456936

Pull #8908

github

web-flow
Merge 737312559 into 8c459cf12
Pull Request #8908: feat: Controller.php Add custom Config\Validation class

5 of 6 new or added lines in 1 file covered. (83.33%)

85 existing lines in 5 files now uncovered.

20357 of 24106 relevant lines covered (84.45%)

188.44 hits per line

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

85.75
/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|object|resource
210
     * @phpstan-var false|TConnection
211
     */
212
    public $connID = false;
213

214
    /**
215
     * Result ID
216
     *
217
     * @var         false|object|resource
218
     * @phpstan-var false|TResult
219
     */
220
    public $resultID = false;
221

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

340
    /**
341
     * Array of table aliases.
342
     *
343
     * @var array
344
     */
345
    protected $aliasedTables = [];
346

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

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

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

377
        foreach ($params as $key => $value) {
361✔
378
            if (property_exists($this, $key)) {
104✔
379
                $this->{$key} = $value;
104✔
380
            }
381
        }
382

383
        $queryClass = str_replace('Connection', 'Query', static::class);
361✔
384

385
        if (class_exists($queryClass)) {
361✔
386
            $this->queryClass = $queryClass;
313✔
387
        }
388

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

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

416
        $this->connectTime = microtime(true);
37✔
417
        $connectionErrors  = [];
37✔
418

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

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

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

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

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

475
        $this->connectDuration = microtime(true) - $this->connectTime;
35✔
476
    }
477

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

489
    /**
490
     * Platform dependent way method for closing the connection.
491
     *
492
     * @return void
493
     */
494
    abstract protected function _close();
495

496
    /**
497
     * Create a persistent database connection.
498
     *
499
     * @return         false|object|resource
500
     * @phpstan-return false|TConnection
501
     */
502
    public function persistentConnect()
503
    {
UNCOV
504
        return $this->connect(true);
×
505
    }
506

507
    /**
508
     * Returns the actual connection object. If both a 'read' and 'write'
509
     * connection has been specified, you can pass either term in to
510
     * get that connection. If you pass either alias in and only a single
511
     * connection is present, it must return the sole connection.
512
     *
513
     * @return         false|object|resource
514
     * @phpstan-return TConnection
515
     */
516
    public function getConnection(?string $alias = null)
517
    {
518
        // @todo work with read/write connections
519
        return $this->connID;
2✔
520
    }
521

522
    /**
523
     * Returns the name of the current database being used.
524
     */
525
    public function getDatabase(): string
526
    {
527
        return empty($this->database) ? '' : $this->database;
654✔
528
    }
529

530
    /**
531
     * Set DB Prefix
532
     *
533
     * Set's the DB Prefix to something new without needing to reconnect
534
     *
535
     * @param string $prefix The prefix
536
     */
537
    public function setPrefix(string $prefix = ''): string
538
    {
539
        return $this->DBPrefix = $prefix;
13✔
540
    }
541

542
    /**
543
     * Returns the database prefix.
544
     */
545
    public function getPrefix(): string
546
    {
547
        return $this->DBPrefix;
12✔
548
    }
549

550
    /**
551
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
552
     */
553
    public function getPlatform(): string
554
    {
555
        return $this->DBDriver;
22✔
556
    }
557

558
    /**
559
     * Sets the Table Aliases to use. These are typically
560
     * collected during use of the Builder, and set here
561
     * so queries are built correctly.
562
     *
563
     * @return $this
564
     */
565
    public function setAliasedTables(array $aliases)
566
    {
567
        $this->aliasedTables = $aliases;
833✔
568

569
        return $this;
833✔
570
    }
571

572
    /**
573
     * Add a table alias to our list.
574
     *
575
     * @return $this
576
     */
577
    public function addTableAlias(string $table)
578
    {
579
        if (! in_array($table, $this->aliasedTables, true)) {
23✔
580
            $this->aliasedTables[] = $table;
23✔
581
        }
582

583
        return $this;
23✔
584
    }
585

586
    /**
587
     * Executes the query against the database.
588
     *
589
     * @return         false|object|resource
590
     * @phpstan-return false|TResult
591
     */
592
    abstract protected function execute(string $sql);
593

594
    /**
595
     * Orchestrates a query against the database. Queries must use
596
     * Database\Statement objects to store the query and build it.
597
     * This method works with the cache.
598
     *
599
     * Should automatically handle different connections for read/write
600
     * queries if needed.
601
     *
602
     * @param array|string|null $binds
603
     *
604
     * @return         BaseResult|bool|Query                       BaseResult when “read” type query, bool when “write” type query, Query when prepared query
605
     * @phpstan-return BaseResult<TConnection, TResult>|bool|Query
606
     *
607
     * @todo BC set $queryClass default as null in 4.1
608
     */
609
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
610
    {
611
        $queryClass = $queryClass ?: $this->queryClass;
693✔
612

613
        if (empty($this->connID)) {
693✔
614
            $this->initialize();
3✔
615
        }
616

617
        /**
618
         * @var Query $query
619
         */
620
        $query = new $queryClass($this);
693✔
621

622
        $query->setQuery($sql, $binds, $setEscapeFlags);
693✔
623

624
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
693✔
UNCOV
625
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
626
        }
627

628
        $startTime = microtime(true);
693✔
629

630
        // Always save the last query so we can use
631
        // the getLastQuery() method.
632
        $this->lastQuery = $query;
693✔
633

634
        // If $pretend is true, then we just want to return
635
        // the actual query object here. There won't be
636
        // any results to return.
637
        if ($this->pretend) {
693✔
638
            $query->setDuration($startTime);
8✔
639

640
            return $query;
8✔
641
        }
642

643
        // Run the query for real
644
        try {
645
            $exception      = null;
693✔
646
            $this->resultID = $this->simpleQuery($query->getQuery());
693✔
647
        } catch (DatabaseException $exception) {
10✔
648
            $this->resultID = false;
10✔
649
        }
650

651
        if ($this->resultID === false) {
693✔
652
            $query->setDuration($startTime, $startTime);
42✔
653

654
            // This will trigger a rollback if transactions are being used
655
            if ($this->transDepth !== 0) {
42✔
656
                $this->transStatus = false;
14✔
657
            }
658

659
            if (
660
                $this->DBDebug
42✔
661
                && (
662
                    // Not in transactions
663
                    $this->transDepth === 0
42✔
664
                    // In transactions, do not throw exception by default.
42✔
665
                    || $this->transException
42✔
666
                )
667
            ) {
668
                // We call this function in order to roll-back queries
669
                // if transactions are enabled. If we don't call this here
670
                // the error message will trigger an exit, causing the
671
                // transactions to remain in limbo.
672
                while ($this->transDepth !== 0) {
23✔
673
                    $transDepth = $this->transDepth;
2✔
674
                    $this->transComplete();
2✔
675

676
                    if ($transDepth === $this->transDepth) {
2✔
UNCOV
677
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
UNCOV
678
                        break;
×
679
                    }
680
                }
681

682
                // Let others do something with this query.
683
                Events::trigger('DBQuery', $query);
23✔
684

685
                if ($exception !== null) {
23✔
686
                    throw new DatabaseException(
6✔
687
                        $exception->getMessage(),
6✔
688
                        $exception->getCode(),
6✔
689
                        $exception
6✔
690
                    );
6✔
691
                }
692

693
                return false;
17✔
694
            }
695

696
            // Let others do something with this query.
697
            Events::trigger('DBQuery', $query);
19✔
698

699
            return false;
19✔
700
        }
701

702
        $query->setDuration($startTime);
693✔
703

704
        // Let others do something with this query
705
        Events::trigger('DBQuery', $query);
693✔
706

707
        // resultID is not false, so it must be successful
708
        if ($this->isWriteType($sql)) {
693✔
709
            return true;
660✔
710
        }
711

712
        // query is not write-type, so it must be read-type query; return QueryResult
713
        $resultClass = str_replace('Connection', 'Result', static::class);
693✔
714

715
        return new $resultClass($this->connID, $this->resultID);
693✔
716
    }
717

718
    /**
719
     * Performs a basic query against the database. No binding or caching
720
     * is performed, nor are transactions handled. Simply takes a raw
721
     * query string and returns the database-specific result id.
722
     *
723
     * @return         false|object|resource
724
     * @phpstan-return false|TResult
725
     */
726
    public function simpleQuery(string $sql)
727
    {
728
        if (empty($this->connID)) {
700✔
729
            $this->initialize();
6✔
730
        }
731

732
        return $this->execute($sql);
700✔
733
    }
734

735
    /**
736
     * Disable Transactions
737
     *
738
     * This permits transactions to be disabled at run-time.
739
     */
740
    public function transOff()
741
    {
UNCOV
742
        $this->transEnabled = false;
×
743
    }
744

745
    /**
746
     * Enable/disable Transaction Strict Mode
747
     *
748
     * When strict mode is enabled, if you are running multiple groups of
749
     * transactions, if one group fails all subsequent groups will be
750
     * rolled back.
751
     *
752
     * If strict mode is disabled, each group is treated autonomously,
753
     * meaning a failure of one group will not affect any others
754
     *
755
     * @param bool $mode = true
756
     *
757
     * @return $this
758
     */
759
    public function transStrict(bool $mode = true)
760
    {
761
        $this->transStrict = $mode;
3✔
762

763
        return $this;
3✔
764
    }
765

766
    /**
767
     * Start Transaction
768
     */
769
    public function transStart(bool $testMode = false): bool
770
    {
771
        if (! $this->transEnabled) {
42✔
UNCOV
772
            return false;
×
773
        }
774

775
        return $this->transBegin($testMode);
42✔
776
    }
777

778
    /**
779
     * If set to true, exceptions are thrown during transactions.
780
     *
781
     * @return $this
782
     */
783
    public function transException(bool $transExcetion)
784
    {
785
        $this->transException = $transExcetion;
3✔
786

787
        return $this;
3✔
788
    }
789

790
    /**
791
     * Complete Transaction
792
     */
793
    public function transComplete(): bool
794
    {
795
        if (! $this->transEnabled) {
42✔
UNCOV
796
            return false;
×
797
        }
798

799
        // The query() function will set this flag to FALSE in the event that a query failed
800
        if ($this->transStatus === false || $this->transFailure === true) {
42✔
801
            $this->transRollback();
12✔
802

803
            // If we are NOT running in strict mode, we will reset
804
            // the _trans_status flag so that subsequent groups of
805
            // transactions will be permitted.
806
            if ($this->transStrict === false) {
12✔
807
                $this->transStatus = true;
3✔
808
            }
809

810
            return false;
12✔
811
        }
812

813
        return $this->transCommit();
33✔
814
    }
815

816
    /**
817
     * Lets you retrieve the transaction flag to determine if it has failed
818
     */
819
    public function transStatus(): bool
820
    {
821
        return $this->transStatus;
11✔
822
    }
823

824
    /**
825
     * Begin Transaction
826
     */
827
    public function transBegin(bool $testMode = false): bool
828
    {
829
        if (! $this->transEnabled) {
44✔
UNCOV
830
            return false;
×
831
        }
832

833
        // When transactions are nested we only begin/commit/rollback the outermost ones
834
        if ($this->transDepth > 0) {
44✔
UNCOV
835
            $this->transDepth++;
×
836

UNCOV
837
            return true;
×
838
        }
839

840
        if (empty($this->connID)) {
44✔
UNCOV
841
            $this->initialize();
×
842
        }
843

844
        // Reset the transaction failure flag.
845
        // If the $test_mode flag is set to TRUE transactions will be rolled back
846
        // even if the queries produce a successful result.
847
        $this->transFailure = ($testMode === true);
44✔
848

849
        if ($this->_transBegin()) {
44✔
850
            $this->transDepth++;
44✔
851

852
            return true;
44✔
853
        }
854

UNCOV
855
        return false;
×
856
    }
857

858
    /**
859
     * Commit Transaction
860
     */
861
    public function transCommit(): bool
862
    {
863
        if (! $this->transEnabled || $this->transDepth === 0) {
33✔
864
            return false;
×
865
        }
866

867
        // When transactions are nested we only begin/commit/rollback the outermost ones
868
        if ($this->transDepth > 1 || $this->_transCommit()) {
33✔
869
            $this->transDepth--;
33✔
870

871
            return true;
33✔
872
        }
873

UNCOV
874
        return false;
×
875
    }
876

877
    /**
878
     * Rollback Transaction
879
     */
880
    public function transRollback(): bool
881
    {
882
        if (! $this->transEnabled || $this->transDepth === 0) {
14✔
883
            return false;
×
884
        }
885

886
        // When transactions are nested we only begin/commit/rollback the outermost ones
887
        if ($this->transDepth > 1 || $this->_transRollback()) {
14✔
888
            $this->transDepth--;
14✔
889

890
            return true;
14✔
891
        }
892

UNCOV
893
        return false;
×
894
    }
895

896
    /**
897
     * Begin Transaction
898
     */
899
    abstract protected function _transBegin(): bool;
900

901
    /**
902
     * Commit Transaction
903
     */
904
    abstract protected function _transCommit(): bool;
905

906
    /**
907
     * Rollback Transaction
908
     */
909
    abstract protected function _transRollback(): bool;
910

911
    /**
912
     * Returns a non-shared new instance of the query builder for this connection.
913
     *
914
     * @param array|string $tableName
915
     *
916
     * @return BaseBuilder
917
     *
918
     * @throws DatabaseException
919
     */
920
    public function table($tableName)
921
    {
922
        if (empty($tableName)) {
782✔
UNCOV
923
            throw new DatabaseException('You must set the database table to be used with your query.');
×
924
        }
925

926
        $className = str_replace('Connection', 'Builder', static::class);
782✔
927

928
        return new $className($tableName, $this);
782✔
929
    }
930

931
    /**
932
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
933
     */
934
    public function newQuery(): BaseBuilder
935
    {
936
        // save table aliases
937
        $tempAliases         = $this->aliasedTables;
14✔
938
        $builder             = $this->table(',')->from([], true);
14✔
939
        $this->aliasedTables = $tempAliases;
14✔
940

941
        return $builder;
14✔
942
    }
943

944
    /**
945
     * Creates a prepared statement with the database that can then
946
     * be used to execute multiple statements against. Within the
947
     * closure, you would build the query in any normal way, though
948
     * the Query Builder is the expected manner.
949
     *
950
     * Example:
951
     *    $stmt = $db->prepare(function($db)
952
     *           {
953
     *             return $db->table('users')
954
     *                   ->where('id', 1)
955
     *                     ->get();
956
     *           })
957
     *
958
     * @return BasePreparedQuery|null
959
     */
960
    public function prepare(Closure $func, array $options = [])
961
    {
962
        if (empty($this->connID)) {
12✔
UNCOV
963
            $this->initialize();
×
964
        }
965

966
        $this->pretend();
12✔
967

968
        $sql = $func($this);
12✔
969

970
        $this->pretend(false);
12✔
971

972
        if ($sql instanceof QueryInterface) {
12✔
973
            $sql = $sql->getOriginalQuery();
12✔
974
        }
975

976
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
12✔
977
        /** @var BasePreparedQuery $class */
978
        $class = new $class($this);
12✔
979

980
        return $class->prepare($sql, $options);
12✔
981
    }
982

983
    /**
984
     * Returns the last query's statement object.
985
     *
986
     * @return Query
987
     */
988
    public function getLastQuery()
989
    {
990
        return $this->lastQuery;
11✔
991
    }
992

993
    /**
994
     * Returns a string representation of the last query's statement object.
995
     */
996
    public function showLastQuery(): string
997
    {
UNCOV
998
        return (string) $this->lastQuery;
×
999
    }
1000

1001
    /**
1002
     * Returns the time we started to connect to this database in
1003
     * seconds with microseconds.
1004
     *
1005
     * Used by the Debug Toolbar's timeline.
1006
     */
1007
    public function getConnectStart(): ?float
1008
    {
1009
        return $this->connectTime;
1✔
1010
    }
1011

1012
    /**
1013
     * Returns the number of seconds with microseconds that it took
1014
     * to connect to the database.
1015
     *
1016
     * Used by the Debug Toolbar's timeline.
1017
     */
1018
    public function getConnectDuration(int $decimals = 6): string
1019
    {
1020
        return number_format($this->connectDuration, $decimals);
2✔
1021
    }
1022

1023
    /**
1024
     * Protect Identifiers
1025
     *
1026
     * This function is used extensively by the Query Builder class, and by
1027
     * a couple functions in this class.
1028
     * It takes a column or table name (optionally with an alias) and inserts
1029
     * the table prefix onto it. Some logic is necessary in order to deal with
1030
     * column names that include the path. Consider a query like this:
1031
     *
1032
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1033
     *
1034
     * Or a query with aliasing:
1035
     *
1036
     * SELECT m.member_id, m.member_name FROM members AS m
1037
     *
1038
     * Since the column name can include up to four segments (host, DB, table, column)
1039
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1040
     * insert the table prefix (if it exists) in the proper position, and escape only
1041
     * the correct identifiers.
1042
     *
1043
     * @param array|int|string $item
1044
     * @param bool             $prefixSingle       Prefix a table name with no segments?
1045
     * @param bool             $protectIdentifiers Protect table or column names?
1046
     * @param bool             $fieldExists        Supplied $item contains a column name?
1047
     *
1048
     * @return         array|string
1049
     * @phpstan-return ($item is array ? array : string)
1050
     */
1051
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1052
    {
1053
        if (! is_bool($protectIdentifiers)) {
938✔
1054
            $protectIdentifiers = $this->protectIdentifiers;
905✔
1055
        }
1056

1057
        if (is_array($item)) {
938✔
1058
            $escapedArray = [];
1✔
1059

1060
            foreach ($item as $k => $v) {
1✔
1061
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1062
            }
1063

1064
            return $escapedArray;
1✔
1065
        }
1066

1067
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1068
        $item = (string) $item;
938✔
1069

1070
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1071
        // If a parenthesis is found we know that we do not need to
1072
        // escape the data or add a prefix. There's probably a more graceful
1073
        // way to deal with this, but I'm not thinking of it
1074
        //
1075
        // Added exception for single quotes as well, we don't want to alter
1076
        // literal strings.
1077
        if (strcspn($item, "()'") !== strlen($item)) {
938✔
1078
            /** @psalm-suppress NoValue I don't know why ERROR. */
1079
            return $item;
664✔
1080
        }
1081

1082
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1083
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
927✔
1084
            /** @psalm-suppress NoValue I don't know why ERROR. */
1085
            return $item;
89✔
1086
        }
1087

1088
        // Convert tabs or multiple spaces into single spaces
1089
        /** @psalm-suppress NoValue I don't know why ERROR. */
1090
        $item = preg_replace('/\s+/', ' ', trim($item));
926✔
1091

1092
        // If the item has an alias declaration we remove it and set it aside.
1093
        // Note: strripos() is used in order to support spaces in table names
1094
        if ($offset = strripos($item, ' AS ')) {
926✔
1095
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1096
            $item  = substr($item, 0, $offset);
11✔
1097
        } elseif ($offset = strrpos($item, ' ')) {
921✔
1098
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
11✔
1099
            $item  = substr($item, 0, $offset);
11✔
1100
        } else {
1101
            $alias = '';
916✔
1102
        }
1103

1104
        // Break the string apart if it contains periods, then insert the table prefix
1105
        // in the correct location, assuming the period doesn't indicate that we're dealing
1106
        // with an alias. While we're at it, we will escape the components
1107
        if (str_contains($item, '.')) {
926✔
1108
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
131✔
1109
        }
1110

1111
        // In some cases, especially 'from', we end up running through
1112
        // protect_identifiers twice. This algorithm won't work when
1113
        // it contains the escapeChar so strip it out.
1114
        $item = trim($item, $this->escapeChar);
918✔
1115

1116
        // Is there a table prefix? If not, no need to insert it
1117
        if ($this->DBPrefix !== '') {
918✔
1118
            // Verify table prefix and replace if necessary
1119
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
700✔
UNCOV
1120
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1121
            }
1122
            // Do we prefix an item with no segments?
1123
            elseif ($prefixSingle === true && ! str_starts_with($item, $this->DBPrefix)) {
700✔
1124
                $item = $this->DBPrefix . $item;
693✔
1125
            }
1126
        }
1127

1128
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
918✔
1129
            $item = $this->escapeIdentifiers($item);
916✔
1130
        }
1131

1132
        return $item . $alias;
918✔
1133
    }
1134

1135
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1136
    {
1137
        $parts = explode('.', $item);
131✔
1138

1139
        // Does the first segment of the exploded item match
1140
        // one of the aliases previously identified? If so,
1141
        // we have nothing more to do other than escape the item
1142
        //
1143
        // NOTE: The ! empty() condition prevents this method
1144
        // from breaking when QB isn't enabled.
1145
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
131✔
1146
            if ($protectIdentifiers === true) {
10✔
1147
                foreach ($parts as $key => $val) {
10✔
1148
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10✔
1149
                        $parts[$key] = $this->escapeIdentifiers($val);
10✔
1150
                    }
1151
                }
1152

1153
                $item = implode('.', $parts);
10✔
1154
            }
1155

1156
            return $item . $alias;
10✔
1157
        }
1158

1159
        // Is there a table prefix defined in the config file? If not, no need to do anything
1160
        if ($this->DBPrefix !== '') {
125✔
1161
            // We now add the table prefix based on some logic.
1162
            // Do we have 4 segments (hostname.database.table.column)?
1163
            // If so, we add the table prefix to the column name in the 3rd segment.
1164
            if (isset($parts[3])) {
117✔
UNCOV
1165
                $i = 2;
×
1166
            }
1167
            // Do we have 3 segments (database.table.column)?
1168
            // If so, we add the table prefix to the column name in 2nd position
1169
            elseif (isset($parts[2])) {
117✔
UNCOV
1170
                $i = 1;
×
1171
            }
1172
            // Do we have 2 segments (table.column)?
1173
            // If so, we add the table prefix to the column name in 1st segment
1174
            else {
1175
                $i = 0;
117✔
1176
            }
1177

1178
            // This flag is set when the supplied $item does not contain a field name.
1179
            // This can happen when this function is being called from a JOIN.
1180
            if ($fieldExists === false) {
117✔
UNCOV
1181
                $i++;
×
1182
            }
1183

1184
            // Verify table prefix and replace if necessary
1185
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
117✔
UNCOV
1186
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1187
            }
1188
            // We only add the table prefix if it does not already exist
1189
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
117✔
1190
                $parts[$i] = $this->DBPrefix . $parts[$i];
117✔
1191
            }
1192

1193
            // Put the parts back together
1194
            $item = implode('.', $parts);
117✔
1195
        }
1196

1197
        if ($protectIdentifiers === true) {
125✔
1198
            $item = $this->escapeIdentifiers($item);
125✔
1199
        }
1200

1201
        return $item . $alias;
125✔
1202
    }
1203

1204
    /**
1205
     * Escape the SQL Identifier
1206
     *
1207
     * This function escapes single identifier.
1208
     *
1209
     * @param non-empty-string $item
1210
     */
1211
    public function escapeIdentifier(string $item): string
1212
    {
1213
        return $this->escapeChar
592✔
1214
            . str_replace(
592✔
1215
                $this->escapeChar,
592✔
1216
                $this->escapeChar . $this->escapeChar,
592✔
1217
                $item
592✔
1218
            )
592✔
1219
            . $this->escapeChar;
592✔
1220
    }
1221

1222
    /**
1223
     * Escape the SQL Identifiers
1224
     *
1225
     * This function escapes column and table names
1226
     *
1227
     * @param array|string $item
1228
     *
1229
     * @return         array|string
1230
     * @phpstan-return ($item is array ? array : string)
1231
     */
1232
    public function escapeIdentifiers($item)
1233
    {
1234
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
934✔
1235
            return $item;
5✔
1236
        }
1237

1238
        if (is_array($item)) {
933✔
1239
            foreach ($item as $key => $value) {
610✔
1240
                $item[$key] = $this->escapeIdentifiers($value);
610✔
1241
            }
1242

1243
            return $item;
610✔
1244
        }
1245

1246
        // Avoid breaking functions and literal values inside queries
1247
        if (ctype_digit($item)
933✔
1248
            || $item[0] === "'"
932✔
1249
            || ($this->escapeChar !== '"' && $item[0] === '"')
932✔
1250
            || str_contains($item, '(')) {
933✔
1251
            return $item;
39✔
1252
        }
1253

1254
        if ($this->pregEscapeChar === []) {
932✔
1255
            if (is_array($this->escapeChar)) {
259✔
UNCOV
1256
                $this->pregEscapeChar = [
×
UNCOV
1257
                    preg_quote($this->escapeChar[0], '/'),
×
UNCOV
1258
                    preg_quote($this->escapeChar[1], '/'),
×
UNCOV
1259
                    $this->escapeChar[0],
×
UNCOV
1260
                    $this->escapeChar[1],
×
UNCOV
1261
                ];
×
1262
            } else {
1263
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
259✔
1264
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
259✔
1265
            }
1266
        }
1267

1268
        foreach ($this->reservedIdentifiers as $id) {
932✔
1269
            /** @psalm-suppress NoValue I don't know why ERROR. */
1270
            if (str_contains($item, '.' . $id)) {
932✔
1271
                return preg_replace(
3✔
1272
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1273
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1274
                    $item
3✔
1275
                );
3✔
1276
            }
1277
        }
1278

1279
        /** @psalm-suppress NoValue I don't know why ERROR. */
1280
        return preg_replace(
930✔
1281
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
930✔
1282
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
930✔
1283
            $item
930✔
1284
        );
930✔
1285
    }
1286

1287
    /**
1288
     * Prepends a database prefix if one exists in configuration
1289
     *
1290
     * @throws DatabaseException
1291
     */
1292
    public function prefixTable(string $table = ''): string
1293
    {
1294
        if ($table === '') {
3✔
UNCOV
1295
            throw new DatabaseException('A table name is required for that operation.');
×
1296
        }
1297

1298
        return $this->DBPrefix . $table;
3✔
1299
    }
1300

1301
    /**
1302
     * Returns the total number of rows affected by this query.
1303
     */
1304
    abstract public function affectedRows(): int;
1305

1306
    /**
1307
     * "Smart" Escape String
1308
     *
1309
     * Escapes data based on type.
1310
     * Sets boolean and null types
1311
     *
1312
     * @param array|bool|float|int|object|string|null $str
1313
     *
1314
     * @return         array|float|int|string
1315
     * @phpstan-return ($str is array ? array : float|int|string)
1316
     */
1317
    public function escape($str)
1318
    {
1319
        if (is_array($str)) {
793✔
1320
            return array_map($this->escape(...), $str);
625✔
1321
        }
1322

1323
        if ($str instanceof Stringable) {
793✔
1324
            if ($str instanceof RawSql) {
13✔
1325
                return $str->__toString();
12✔
1326
            }
1327

1328
            $str = (string) $str;
1✔
1329
        }
1330

1331
        if (is_string($str)) {
790✔
1332
            return "'" . $this->escapeString($str) . "'";
740✔
1333
        }
1334

1335
        if (is_bool($str)) {
712✔
1336
            return ($str === false) ? 0 : 1;
6✔
1337
        }
1338

1339
        return $str ?? 'NULL';
710✔
1340
    }
1341

1342
    /**
1343
     * Escape String
1344
     *
1345
     * @param list<string|Stringable>|string|Stringable $str  Input string
1346
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1347
     *
1348
     * @return list<string>|string
1349
     */
1350
    public function escapeString($str, bool $like = false)
1351
    {
1352
        if (is_array($str)) {
740✔
UNCOV
1353
            foreach ($str as $key => $val) {
×
UNCOV
1354
                $str[$key] = $this->escapeString($val, $like);
×
1355
            }
1356

UNCOV
1357
            return $str;
×
1358
        }
1359

1360
        if ($str instanceof Stringable) {
740✔
1361
            if ($str instanceof RawSql) {
2✔
UNCOV
1362
                return $str->__toString();
×
1363
            }
1364

1365
            $str = (string) $str;
2✔
1366
        }
1367

1368
        $str = $this->_escapeString($str);
740✔
1369

1370
        // escape LIKE condition wildcards
1371
        if ($like === true) {
740✔
1372
            return str_replace(
2✔
1373
                [
2✔
1374
                    $this->likeEscapeChar,
2✔
1375
                    '%',
2✔
1376
                    '_',
2✔
1377
                ],
2✔
1378
                [
2✔
1379
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1380
                    $this->likeEscapeChar . '%',
2✔
1381
                    $this->likeEscapeChar . '_',
2✔
1382
                ],
2✔
1383
                $str
2✔
1384
            );
2✔
1385
        }
1386

1387
        return $str;
740✔
1388
    }
1389

1390
    /**
1391
     * Escape LIKE String
1392
     *
1393
     * Calls the individual driver for platform
1394
     * specific escaping for LIKE conditions
1395
     *
1396
     * @param list<string|Stringable>|string|Stringable $str
1397
     *
1398
     * @return list<string>|string
1399
     */
1400
    public function escapeLikeString($str)
1401
    {
1402
        return $this->escapeString($str, true);
2✔
1403
    }
1404

1405
    /**
1406
     * Platform independent string escape.
1407
     *
1408
     * Will likely be overridden in child classes.
1409
     */
1410
    protected function _escapeString(string $str): string
1411
    {
1412
        return str_replace("'", "''", remove_invisible_characters($str, false));
702✔
1413
    }
1414

1415
    /**
1416
     * This function enables you to call PHP database functions that are not natively included
1417
     * in CodeIgniter, in a platform independent manner.
1418
     *
1419
     * @param array ...$params
1420
     *
1421
     * @throws DatabaseException
1422
     */
1423
    public function callFunction(string $functionName, ...$params): bool
1424
    {
UNCOV
1425
        $driver = $this->getDriverFunctionPrefix();
×
1426

UNCOV
1427
        if (! str_contains($driver, $functionName)) {
×
UNCOV
1428
            $functionName = $driver . $functionName;
×
1429
        }
1430

UNCOV
1431
        if (! function_exists($functionName)) {
×
UNCOV
1432
            if ($this->DBDebug) {
×
UNCOV
1433
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1434
            }
1435

UNCOV
1436
            return false;
×
1437
        }
1438

UNCOV
1439
        return $functionName(...$params);
×
1440
    }
1441

1442
    /**
1443
     * Get the prefix of the function to access the DB.
1444
     */
1445
    protected function getDriverFunctionPrefix(): string
1446
    {
UNCOV
1447
        return strtolower($this->DBDriver) . '_';
×
1448
    }
1449

1450
    // --------------------------------------------------------------------
1451
    // META Methods
1452
    // --------------------------------------------------------------------
1453

1454
    /**
1455
     * Returns an array of table names
1456
     *
1457
     * @return array|false
1458
     *
1459
     * @throws DatabaseException
1460
     */
1461
    public function listTables(bool $constrainByPrefix = false)
1462
    {
1463
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
655✔
1464
            return $constrainByPrefix
648✔
1465
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1466
                : $this->dataCache['table_names'];
648✔
1467
        }
1468

1469
        $sql = $this->_listTables($constrainByPrefix);
49✔
1470

1471
        if ($sql === false) {
49✔
UNCOV
1472
            if ($this->DBDebug) {
×
UNCOV
1473
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1474
            }
1475

UNCOV
1476
            return false;
×
1477
        }
1478

1479
        $this->dataCache['table_names'] = [];
49✔
1480

1481
        $query = $this->query($sql);
49✔
1482

1483
        foreach ($query->getResultArray() as $row) {
49✔
1484
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
46✔
1485

1486
            $this->dataCache['table_names'][] = $table;
46✔
1487
        }
1488

1489
        return $this->dataCache['table_names'];
49✔
1490
    }
1491

1492
    /**
1493
     * Determine if a particular table exists
1494
     *
1495
     * @param bool $cached Whether to use data cache
1496
     */
1497
    public function tableExists(string $tableName, bool $cached = true): bool
1498
    {
1499
        if ($cached === true) {
649✔
1500
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
648✔
1501
        }
1502

1503
        if (false === ($sql = $this->_listTables(false, $tableName))) {
605✔
UNCOV
1504
            if ($this->DBDebug) {
×
UNCOV
1505
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1506
            }
1507

UNCOV
1508
            return false;
×
1509
        }
1510

1511
        $tableExists = $this->query($sql)->getResultArray() !== [];
605✔
1512

1513
        // if cache has been built already
1514
        if (! empty($this->dataCache['table_names'])) {
605✔
1515
            $key = array_search(
601✔
1516
                strtolower($tableName),
601✔
1517
                array_map('strtolower', $this->dataCache['table_names']),
601✔
1518
                true
601✔
1519
            );
601✔
1520

1521
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1522
            // OR if table does exist but is not found in cache
1523
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
601✔
1524
                $this->resetDataCache();
1✔
1525
            }
1526
        }
1527

1528
        return $tableExists;
605✔
1529
    }
1530

1531
    /**
1532
     * Fetch Field Names
1533
     *
1534
     * @return array|false
1535
     *
1536
     * @throws DatabaseException
1537
     */
1538
    public function getFieldNames(string $table)
1539
    {
1540
        // Is there a cached result?
1541
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1542
            return $this->dataCache['field_names'][$table];
7✔
1543
        }
1544

1545
        if (empty($this->connID)) {
8✔
UNCOV
1546
            $this->initialize();
×
1547
        }
1548

1549
        if (false === ($sql = $this->_listColumns($table))) {
8✔
UNCOV
1550
            if ($this->DBDebug) {
×
UNCOV
1551
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1552
            }
1553

UNCOV
1554
            return false;
×
1555
        }
1556

1557
        $query = $this->query($sql);
8✔
1558

1559
        $this->dataCache['field_names'][$table] = [];
8✔
1560

1561
        foreach ($query->getResultArray() as $row) {
8✔
1562
            // Do we know from where to get the column's name?
1563
            if (! isset($key)) {
8✔
1564
                if (isset($row['column_name'])) {
8✔
1565
                    $key = 'column_name';
8✔
1566
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1567
                    $key = 'COLUMN_NAME';
8✔
1568
                } else {
1569
                    // We have no other choice but to just get the first element's key.
1570
                    $key = key($row);
8✔
1571
                }
1572
            }
1573

1574
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1575
        }
1576

1577
        return $this->dataCache['field_names'][$table];
8✔
1578
    }
1579

1580
    /**
1581
     * Determine if a particular field exists
1582
     */
1583
    public function fieldExists(string $fieldName, string $tableName): bool
1584
    {
1585
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1586
    }
1587

1588
    /**
1589
     * Returns an object with field data
1590
     *
1591
     * @return list<stdClass>
1592
     */
1593
    public function getFieldData(string $table)
1594
    {
1595
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
134✔
1596
    }
1597

1598
    /**
1599
     * Returns an object with key data
1600
     *
1601
     * @return array<string, stdClass>
1602
     */
1603
    public function getIndexData(string $table)
1604
    {
1605
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
147✔
1606
    }
1607

1608
    /**
1609
     * Returns an object with foreign key data
1610
     *
1611
     * @return array
1612
     */
1613
    public function getForeignKeyData(string $table)
1614
    {
1615
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
34✔
1616
    }
1617

1618
    /**
1619
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1620
     *
1621
     * @return array<string, stdClass>
1622
     *
1623
     * array[
1624
     *    {constraint_name} =>
1625
     *        stdClass[
1626
     *            'constraint_name'     => string,
1627
     *            'table_name'          => string,
1628
     *            'column_name'         => string[],
1629
     *            'foreign_table_name'  => string,
1630
     *            'foreign_column_name' => string[],
1631
     *            'on_delete'           => string,
1632
     *            'on_update'           => string,
1633
     *            'match'               => string
1634
     *        ]
1635
     * ]
1636
     */
1637
    protected function foreignKeyDataToObjects(array $data)
1638
    {
1639
        $retVal = [];
34✔
1640

1641
        foreach ($data as $row) {
34✔
1642
            $name = $row['constraint_name'];
12✔
1643

1644
            // for sqlite generate name
1645
            if ($name === null) {
12✔
1646
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1647
            }
1648

1649
            $obj                      = new stdClass();
12✔
1650
            $obj->constraint_name     = $name;
12✔
1651
            $obj->table_name          = $row['table_name'];
12✔
1652
            $obj->column_name         = $row['column_name'];
12✔
1653
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1654
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1655
            $obj->on_delete           = $row['on_delete'];
12✔
1656
            $obj->on_update           = $row['on_update'];
12✔
1657
            $obj->match               = $row['match'];
12✔
1658

1659
            $retVal[$name] = $obj;
12✔
1660
        }
1661

1662
        return $retVal;
34✔
1663
    }
1664

1665
    /**
1666
     * Disables foreign key checks temporarily.
1667
     *
1668
     * @return bool
1669
     */
1670
    public function disableForeignKeyChecks()
1671
    {
1672
        $sql = $this->_disableForeignKeyChecks();
618✔
1673

1674
        if ($sql === '') {
618✔
1675
            // The feature is not supported.
UNCOV
1676
            return false;
×
1677
        }
1678

1679
        return $this->query($sql);
618✔
1680
    }
1681

1682
    /**
1683
     * Enables foreign key checks temporarily.
1684
     *
1685
     * @return bool
1686
     */
1687
    public function enableForeignKeyChecks()
1688
    {
1689
        $sql = $this->_enableForeignKeyChecks();
691✔
1690

1691
        if ($sql === '') {
691✔
1692
            // The feature is not supported.
UNCOV
1693
            return false;
×
1694
        }
1695

1696
        return $this->query($sql);
691✔
1697
    }
1698

1699
    /**
1700
     * Allows the engine to be set into a mode where queries are not
1701
     * actually executed, but they are still generated, timed, etc.
1702
     *
1703
     * This is primarily used by the prepared query functionality.
1704
     *
1705
     * @return $this
1706
     */
1707
    public function pretend(bool $pretend = true)
1708
    {
1709
        $this->pretend = $pretend;
13✔
1710

1711
        return $this;
13✔
1712
    }
1713

1714
    /**
1715
     * Empties our data cache. Especially helpful during testing.
1716
     *
1717
     * @return $this
1718
     */
1719
    public function resetDataCache()
1720
    {
1721
        $this->dataCache = [];
33✔
1722

1723
        return $this;
33✔
1724
    }
1725

1726
    /**
1727
     * Determines if the statement is a write-type query or not.
1728
     *
1729
     * @param string $sql
1730
     */
1731
    public function isWriteType($sql): bool
1732
    {
1733
        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);
717✔
1734
    }
1735

1736
    /**
1737
     * Returns the last error code and message.
1738
     *
1739
     * Must return an array with keys 'code' and 'message':
1740
     *
1741
     * @return         array<string, int|string|null>
1742
     * @phpstan-return array{code: int|string|null, message: string|null}
1743
     */
1744
    abstract public function error(): array;
1745

1746
    /**
1747
     * Insert ID
1748
     *
1749
     * @return int|string
1750
     */
1751
    abstract public function insertID();
1752

1753
    /**
1754
     * Generates the SQL for listing tables in a platform-dependent manner.
1755
     *
1756
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1757
     *
1758
     * @return false|string
1759
     */
1760
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1761

1762
    /**
1763
     * Generates a platform-specific query string so that the column names can be fetched.
1764
     *
1765
     * @return false|string
1766
     */
1767
    abstract protected function _listColumns(string $table = '');
1768

1769
    /**
1770
     * Platform-specific field data information.
1771
     *
1772
     * @see    getFieldData()
1773
     */
1774
    abstract protected function _fieldData(string $table): array;
1775

1776
    /**
1777
     * Platform-specific index data.
1778
     *
1779
     * @see    getIndexData()
1780
     *
1781
     * @return array<string, stdClass>
1782
     */
1783
    abstract protected function _indexData(string $table): array;
1784

1785
    /**
1786
     * Platform-specific foreign keys data.
1787
     *
1788
     * @see    getForeignKeyData()
1789
     *
1790
     * @return array<string, stdClass>
1791
     */
1792
    abstract protected function _foreignKeyData(string $table): array;
1793

1794
    /**
1795
     * Platform-specific SQL statement to disable foreign key checks.
1796
     *
1797
     * If this feature is not supported, return empty string.
1798
     *
1799
     * @TODO This method should be moved to an interface that represents foreign key support.
1800
     *
1801
     * @return string
1802
     *
1803
     * @see disableForeignKeyChecks()
1804
     */
1805
    protected function _disableForeignKeyChecks()
1806
    {
UNCOV
1807
        return '';
×
1808
    }
1809

1810
    /**
1811
     * Platform-specific SQL statement to enable foreign key checks.
1812
     *
1813
     * If this feature is not supported, return empty string.
1814
     *
1815
     * @TODO This method should be moved to an interface that represents foreign key support.
1816
     *
1817
     * @return string
1818
     *
1819
     * @see enableForeignKeyChecks()
1820
     */
1821
    protected function _enableForeignKeyChecks()
1822
    {
UNCOV
1823
        return '';
×
1824
    }
1825

1826
    /**
1827
     * Accessor for properties if they exist.
1828
     *
1829
     * @return array|bool|float|int|object|resource|string|null
1830
     */
1831
    public function __get(string $key)
1832
    {
1833
        if (property_exists($this, $key)) {
887✔
1834
            return $this->{$key};
886✔
1835
        }
1836

1837
        return null;
1✔
1838
    }
1839

1840
    /**
1841
     * Checker for properties existence.
1842
     */
1843
    public function __isset(string $key): bool
1844
    {
1845
        return property_exists($this, $key);
209✔
1846
    }
1847
}
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

© 2025 Coveralls, Inc