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

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

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

85.99
/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 list<string>
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'])) {
391✔
373
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
95✔
374
            unset($params['dateFormat']);
95✔
375
        }
376

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

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

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

389
        if ($this->failover !== []) {
391✔
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, connID can be either
410
         * boolean TRUE, a resource or an object.
411
         */
412
        if ($this->connID) {
726✔
413
            return;
692✔
414
        }
415

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

419
        try {
420
            // Connect to the database and set the connection ID
421
            $this->connID = $this->connect($this->pConnect);
44✔
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) {
44✔
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;
42✔
476
    }
477

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

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

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

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

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

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

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

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

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

571
        return $this;
871✔
572
    }
573

574
    /**
575
     * Add a table alias to our list.
576
     *
577
     * @return $this
578
     */
579
    public function addTableAlias(string $alias)
580
    {
581
        if ($alias === '') {
30✔
582
            return $this;
6✔
583
        }
584

585
        if (! in_array($alias, $this->aliasedTables, true)) {
24✔
586
            $this->aliasedTables[] = $alias;
24✔
587
        }
588

589
        return $this;
24✔
590
    }
591

592
    /**
593
     * Executes the query against the database.
594
     *
595
     * @return         false|object|resource
596
     * @phpstan-return false|TResult
597
     */
598
    abstract protected function execute(string $sql);
599

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

619
        if (empty($this->connID)) {
725✔
620
            $this->initialize();
5✔
621
        }
622

623
        /**
624
         * @var Query $query
625
         */
626
        $query = new $queryClass($this);
725✔
627

628
        $query->setQuery($sql, $binds, $setEscapeFlags);
725✔
629

630
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
725✔
631
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
632
        }
633

634
        $startTime = microtime(true);
725✔
635

636
        // Always save the last query so we can use
637
        // the getLastQuery() method.
638
        $this->lastQuery = $query;
725✔
639

640
        // If $pretend is true, then we just want to return
641
        // the actual query object here. There won't be
642
        // any results to return.
643
        if ($this->pretend) {
725✔
644
            $query->setDuration($startTime);
9✔
645

646
            return $query;
9✔
647
        }
648

649
        // Run the query for real
650
        try {
651
            $exception      = null;
725✔
652
            $this->resultID = $this->simpleQuery($query->getQuery());
725✔
653
        } catch (DatabaseException $exception) {
13✔
654
            $this->resultID = false;
13✔
655
        }
656

657
        if ($this->resultID === false) {
725✔
658
            $query->setDuration($startTime, $startTime);
31✔
659

660
            // This will trigger a rollback if transactions are being used
661
            if ($this->transDepth !== 0) {
31✔
662
                $this->transStatus = false;
17✔
663
            }
664

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

682
                    if ($transDepth === $this->transDepth) {
2✔
683
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
684
                        break;
×
685
                    }
686
                }
687

688
                // Let others do something with this query.
689
                Events::trigger('DBQuery', $query);
9✔
690

691
                if ($exception instanceof DatabaseException) {
9✔
692
                    throw new DatabaseException(
7✔
693
                        $exception->getMessage(),
7✔
694
                        $exception->getCode(),
7✔
695
                        $exception
7✔
696
                    );
7✔
697
                }
698

699
                return false;
2✔
700
            }
701

702
            // Let others do something with this query.
703
            Events::trigger('DBQuery', $query);
22✔
704

705
            return false;
22✔
706
        }
707

708
        $query->setDuration($startTime);
725✔
709

710
        // Let others do something with this query
711
        Events::trigger('DBQuery', $query);
725✔
712

713
        // resultID is not false, so it must be successful
714
        if ($this->isWriteType($sql)) {
725✔
715
            return true;
691✔
716
        }
717

718
        // query is not write-type, so it must be read-type query; return QueryResult
719
        $resultClass = str_replace('Connection', 'Result', static::class);
725✔
720

721
        return new $resultClass($this->connID, $this->resultID);
725✔
722
    }
723

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

738
        return $this->execute($sql);
732✔
739
    }
740

741
    /**
742
     * Disable Transactions
743
     *
744
     * This permits transactions to be disabled at run-time.
745
     *
746
     * @return void
747
     */
748
    public function transOff()
749
    {
750
        $this->transEnabled = false;
×
751
    }
752

753
    /**
754
     * Enable/disable Transaction Strict Mode
755
     *
756
     * When strict mode is enabled, if you are running multiple groups of
757
     * transactions, if one group fails all subsequent groups will be
758
     * rolled back.
759
     *
760
     * If strict mode is disabled, each group is treated autonomously,
761
     * meaning a failure of one group will not affect any others
762
     *
763
     * @param bool $mode = true
764
     *
765
     * @return $this
766
     */
767
    public function transStrict(bool $mode = true)
768
    {
769
        $this->transStrict = $mode;
4✔
770

771
        return $this;
4✔
772
    }
773

774
    /**
775
     * Start Transaction
776
     */
777
    public function transStart(bool $testMode = false): bool
778
    {
779
        if (! $this->transEnabled) {
44✔
780
            return false;
×
781
        }
782

783
        return $this->transBegin($testMode);
44✔
784
    }
785

786
    /**
787
     * If set to true, exceptions are thrown during transactions.
788
     *
789
     * @return $this
790
     */
791
    public function transException(bool $transException)
792
    {
793
        $this->transException = $transException;
3✔
794

795
        return $this;
3✔
796
    }
797

798
    /**
799
     * Complete Transaction
800
     */
801
    public function transComplete(): bool
802
    {
803
        if (! $this->transEnabled) {
45✔
804
            return false;
×
805
        }
806

807
        // The query() function will set this flag to FALSE in the event that a query failed
808
        if ($this->transStatus === false || $this->transFailure === true) {
45✔
809
            $this->transRollback();
15✔
810

811
            // If we are NOT running in strict mode, we will reset
812
            // the _trans_status flag so that subsequent groups of
813
            // transactions will be permitted.
814
            if ($this->transStrict === false) {
15✔
815
                $this->transStatus = true;
4✔
816
            }
817

818
            return false;
15✔
819
        }
820

821
        return $this->transCommit();
35✔
822
    }
823

824
    /**
825
     * Lets you retrieve the transaction flag to determine if it has failed
826
     */
827
    public function transStatus(): bool
828
    {
829
        return $this->transStatus;
14✔
830
    }
831

832
    /**
833
     * Begin Transaction
834
     */
835
    public function transBegin(bool $testMode = false): bool
836
    {
837
        if (! $this->transEnabled) {
47✔
838
            return false;
×
839
        }
840

841
        // When transactions are nested we only begin/commit/rollback the outermost ones
842
        if ($this->transDepth > 0) {
47✔
843
            $this->transDepth++;
×
844

845
            return true;
×
846
        }
847

848
        if (empty($this->connID)) {
47✔
849
            $this->initialize();
×
850
        }
851

852
        // Reset the transaction failure flag.
853
        // If the $testMode flag is set to TRUE transactions will be rolled back
854
        // even if the queries produce a successful result.
855
        $this->transFailure = $testMode;
47✔
856

857
        if ($this->_transBegin()) {
47✔
858
            $this->transDepth++;
47✔
859

860
            return true;
47✔
861
        }
862

863
        return false;
×
864
    }
865

866
    /**
867
     * Commit Transaction
868
     */
869
    public function transCommit(): bool
870
    {
871
        if (! $this->transEnabled || $this->transDepth === 0) {
35✔
872
            return false;
×
873
        }
874

875
        // When transactions are nested we only begin/commit/rollback the outermost ones
876
        if ($this->transDepth > 1 || $this->_transCommit()) {
35✔
877
            $this->transDepth--;
35✔
878

879
            return true;
35✔
880
        }
881

882
        return false;
×
883
    }
884

885
    /**
886
     * Rollback Transaction
887
     */
888
    public function transRollback(): bool
889
    {
890
        if (! $this->transEnabled || $this->transDepth === 0) {
17✔
891
            return false;
×
892
        }
893

894
        // When transactions are nested we only begin/commit/rollback the outermost ones
895
        if ($this->transDepth > 1 || $this->_transRollback()) {
17✔
896
            $this->transDepth--;
17✔
897

898
            return true;
17✔
899
        }
900

901
        return false;
×
902
    }
903

904
    /**
905
     * Reset transaction status - to restart transactions after strict mode failure
906
     */
907
    public function resetTransStatus(): static
908
    {
909
        $this->transStatus = true;
2✔
910

911
        return $this;
2✔
912
    }
913

914
    /**
915
     * Begin Transaction
916
     */
917
    abstract protected function _transBegin(): bool;
918

919
    /**
920
     * Commit Transaction
921
     */
922
    abstract protected function _transCommit(): bool;
923

924
    /**
925
     * Rollback Transaction
926
     */
927
    abstract protected function _transRollback(): bool;
928

929
    /**
930
     * Returns a non-shared new instance of the query builder for this connection.
931
     *
932
     * @param array|string|TableName $tableName
933
     *
934
     * @return BaseBuilder
935
     *
936
     * @throws DatabaseException
937
     */
938
    public function table($tableName)
939
    {
940
        if (empty($tableName)) {
814✔
941
            throw new DatabaseException('You must set the database table to be used with your query.');
×
942
        }
943

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

946
        return new $className($tableName, $this);
814✔
947
    }
948

949
    /**
950
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
951
     */
952
    public function newQuery(): BaseBuilder
953
    {
954
        // save table aliases
955
        $tempAliases         = $this->aliasedTables;
14✔
956
        $builder             = $this->table(',')->from([], true);
14✔
957
        $this->aliasedTables = $tempAliases;
14✔
958

959
        return $builder;
14✔
960
    }
961

962
    /**
963
     * Creates a prepared statement with the database that can then
964
     * be used to execute multiple statements against. Within the
965
     * closure, you would build the query in any normal way, though
966
     * the Query Builder is the expected manner.
967
     *
968
     * Example:
969
     *    $stmt = $db->prepare(function($db)
970
     *           {
971
     *             return $db->table('users')
972
     *                   ->where('id', 1)
973
     *                     ->get();
974
     *           })
975
     *
976
     * @param Closure(BaseConnection): mixed $func
977
     *
978
     * @return BasePreparedQuery|null
979
     */
980
    public function prepare(Closure $func, array $options = [])
981
    {
982
        if (empty($this->connID)) {
13✔
983
            $this->initialize();
×
984
        }
985

986
        $this->pretend();
13✔
987

988
        $sql = $func($this);
13✔
989

990
        $this->pretend(false);
13✔
991

992
        if ($sql instanceof QueryInterface) {
13✔
993
            $sql = $sql->getOriginalQuery();
13✔
994
        }
995

996
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
13✔
997
        /** @var BasePreparedQuery $class */
998
        $class = new $class($this);
13✔
999

1000
        return $class->prepare($sql, $options);
13✔
1001
    }
1002

1003
    /**
1004
     * Returns the last query's statement object.
1005
     *
1006
     * @return Query
1007
     */
1008
    public function getLastQuery()
1009
    {
1010
        return $this->lastQuery;
11✔
1011
    }
1012

1013
    /**
1014
     * Returns a string representation of the last query's statement object.
1015
     */
1016
    public function showLastQuery(): string
1017
    {
1018
        return (string) $this->lastQuery;
×
1019
    }
1020

1021
    /**
1022
     * Returns the time we started to connect to this database in
1023
     * seconds with microseconds.
1024
     *
1025
     * Used by the Debug Toolbar's timeline.
1026
     */
1027
    public function getConnectStart(): ?float
1028
    {
1029
        return $this->connectTime;
1✔
1030
    }
1031

1032
    /**
1033
     * Returns the number of seconds with microseconds that it took
1034
     * to connect to the database.
1035
     *
1036
     * Used by the Debug Toolbar's timeline.
1037
     */
1038
    public function getConnectDuration(int $decimals = 6): string
1039
    {
1040
        return number_format($this->connectDuration, $decimals);
2✔
1041
    }
1042

1043
    /**
1044
     * Protect Identifiers
1045
     *
1046
     * This function is used extensively by the Query Builder class, and by
1047
     * a couple functions in this class.
1048
     * It takes a column or table name (optionally with an alias) and inserts
1049
     * the table prefix onto it. Some logic is necessary in order to deal with
1050
     * column names that include the path. Consider a query like this:
1051
     *
1052
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1053
     *
1054
     * Or a query with aliasing:
1055
     *
1056
     * SELECT m.member_id, m.member_name FROM members AS m
1057
     *
1058
     * Since the column name can include up to four segments (host, DB, table, column)
1059
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1060
     * insert the table prefix (if it exists) in the proper position, and escape only
1061
     * the correct identifiers.
1062
     *
1063
     * @param array|int|string|TableName $item
1064
     * @param bool                       $prefixSingle       Prefix a table name with no segments?
1065
     * @param bool                       $protectIdentifiers Protect table or column names?
1066
     * @param bool                       $fieldExists        Supplied $item contains a column name?
1067
     *
1068
     * @return         array|string
1069
     * @phpstan-return ($item is array ? array : string)
1070
     */
1071
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1072
    {
1073
        if (! is_bool($protectIdentifiers)) {
976✔
1074
            $protectIdentifiers = $this->protectIdentifiers;
943✔
1075
        }
1076

1077
        if (is_array($item)) {
976✔
1078
            $escapedArray = [];
1✔
1079

1080
            foreach ($item as $k => $v) {
1✔
1081
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1082
            }
1083

1084
            return $escapedArray;
1✔
1085
        }
1086

1087
        if ($item instanceof TableName) {
976✔
1088
            /** @psalm-suppress NoValue I don't know why ERROR. */
1089
            return $this->escapeTableName($item);
2✔
1090
        }
1091

1092
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1093
        $item = (string) $item;
976✔
1094

1095
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1096
        // If a parenthesis is found we know that we do not need to
1097
        // escape the data or add a prefix. There's probably a more graceful
1098
        // way to deal with this, but I'm not thinking of it
1099
        //
1100
        // Added exception for single quotes as well, we don't want to alter
1101
        // literal strings.
1102
        if (strcspn($item, "()'") !== strlen($item)) {
976✔
1103
            /** @psalm-suppress NoValue I don't know why ERROR. */
1104
            return $item;
693✔
1105
        }
1106

1107
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1108
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
965✔
1109
            /** @psalm-suppress NoValue I don't know why ERROR. */
1110
            return $item;
96✔
1111
        }
1112

1113
        // Convert tabs or multiple spaces into single spaces
1114
        /** @psalm-suppress NoValue I don't know why ERROR. */
1115
        $item = preg_replace('/\s+/', ' ', trim($item));
964✔
1116

1117
        // If the item has an alias declaration we remove it and set it aside.
1118
        // Note: strripos() is used in order to support spaces in table names
1119
        if ($offset = strripos($item, ' AS ')) {
964✔
1120
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1121
            $item  = substr($item, 0, $offset);
11✔
1122
        } elseif ($offset = strrpos($item, ' ')) {
959✔
1123
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
12✔
1124
            $item  = substr($item, 0, $offset);
12✔
1125
        } else {
1126
            $alias = '';
953✔
1127
        }
1128

1129
        // Break the string apart if it contains periods, then insert the table prefix
1130
        // in the correct location, assuming the period doesn't indicate that we're dealing
1131
        // with an alias. While we're at it, we will escape the components
1132
        if (str_contains($item, '.')) {
964✔
1133
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
134✔
1134
        }
1135

1136
        // In some cases, especially 'from', we end up running through
1137
        // protect_identifiers twice. This algorithm won't work when
1138
        // it contains the escapeChar so strip it out.
1139
        $item = trim($item, $this->escapeChar);
956✔
1140

1141
        // Is there a table prefix? If not, no need to insert it
1142
        if ($this->DBPrefix !== '') {
956✔
1143
            // Verify table prefix and replace if necessary
1144
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
731✔
1145
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1146
            }
1147
            // Do we prefix an item with no segments?
1148
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
731✔
1149
                $item = $this->DBPrefix . $item;
724✔
1150
            }
1151
        }
1152

1153
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
956✔
1154
            $item = $this->escapeIdentifiers($item);
954✔
1155
        }
1156

1157
        return $item . $alias;
956✔
1158
    }
1159

1160
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1161
    {
1162
        $parts = explode('.', $item);
134✔
1163

1164
        // Does the first segment of the exploded item match
1165
        // one of the aliases previously identified? If so,
1166
        // we have nothing more to do other than escape the item
1167
        //
1168
        // NOTE: The ! empty() condition prevents this method
1169
        // from breaking when QB isn't enabled.
1170
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
134✔
1171
            if ($protectIdentifiers) {
10✔
1172
                foreach ($parts as $key => $val) {
10✔
1173
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10✔
1174
                        $parts[$key] = $this->escapeIdentifiers($val);
10✔
1175
                    }
1176
                }
1177

1178
                $item = implode('.', $parts);
10✔
1179
            }
1180

1181
            return $item . $alias;
10✔
1182
        }
1183

1184
        // Is there a table prefix defined in the config file? If not, no need to do anything
1185
        if ($this->DBPrefix !== '') {
128✔
1186
            // We now add the table prefix based on some logic.
1187
            // Do we have 4 segments (hostname.database.table.column)?
1188
            // If so, we add the table prefix to the column name in the 3rd segment.
1189
            if (isset($parts[3])) {
120✔
1190
                $i = 2;
×
1191
            }
1192
            // Do we have 3 segments (database.table.column)?
1193
            // If so, we add the table prefix to the column name in 2nd position
1194
            elseif (isset($parts[2])) {
120✔
1195
                $i = 1;
×
1196
            }
1197
            // Do we have 2 segments (table.column)?
1198
            // If so, we add the table prefix to the column name in 1st segment
1199
            else {
1200
                $i = 0;
120✔
1201
            }
1202

1203
            // This flag is set when the supplied $item does not contain a field name.
1204
            // This can happen when this function is being called from a JOIN.
1205
            if ($fieldExists === false) {
120✔
1206
                $i++;
×
1207
            }
1208

1209
            // Verify table prefix and replace if necessary
1210
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
120✔
1211
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1212
            }
1213
            // We only add the table prefix if it does not already exist
1214
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
120✔
1215
                $parts[$i] = $this->DBPrefix . $parts[$i];
120✔
1216
            }
1217

1218
            // Put the parts back together
1219
            $item = implode('.', $parts);
120✔
1220
        }
1221

1222
        if ($protectIdentifiers) {
128✔
1223
            $item = $this->escapeIdentifiers($item);
128✔
1224
        }
1225

1226
        return $item . $alias;
128✔
1227
    }
1228

1229
    /**
1230
     * Escape the SQL Identifier
1231
     *
1232
     * This function escapes single identifier.
1233
     *
1234
     * @param non-empty-string|TableName $item
1235
     */
1236
    public function escapeIdentifier($item): string
1237
    {
1238
        if ($item === '') {
626✔
1239
            return '';
×
1240
        }
1241

1242
        if ($item instanceof TableName) {
626✔
1243
            return $this->escapeTableName($item);
7✔
1244
        }
1245

1246
        return $this->escapeChar
626✔
1247
            . str_replace(
626✔
1248
                $this->escapeChar,
626✔
1249
                $this->escapeChar . $this->escapeChar,
626✔
1250
                $item
626✔
1251
            )
626✔
1252
            . $this->escapeChar;
626✔
1253
    }
1254

1255
    /**
1256
     * Returns escaped table name with alias.
1257
     */
1258
    private function escapeTableName(TableName $tableName): string
1259
    {
1260
        $alias = $tableName->getAlias();
7✔
1261

1262
        return $this->escapeIdentifier($tableName->getActualTableName())
7✔
1263
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
7✔
1264
    }
1265

1266
    /**
1267
     * Escape the SQL Identifiers
1268
     *
1269
     * This function escapes column and table names
1270
     *
1271
     * @param array|string $item
1272
     *
1273
     * @return         array|string
1274
     * @phpstan-return ($item is array ? array : string)
1275
     */
1276
    public function escapeIdentifiers($item)
1277
    {
1278
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
972✔
1279
            return $item;
5✔
1280
        }
1281

1282
        if (is_array($item)) {
971✔
1283
            foreach ($item as $key => $value) {
639✔
1284
                $item[$key] = $this->escapeIdentifiers($value);
639✔
1285
            }
1286

1287
            return $item;
639✔
1288
        }
1289

1290
        // Avoid breaking functions and literal values inside queries
1291
        if (ctype_digit($item)
971✔
1292
            || $item[0] === "'"
970✔
1293
            || ($this->escapeChar !== '"' && $item[0] === '"')
970✔
1294
            || str_contains($item, '(')) {
971✔
1295
            return $item;
44✔
1296
        }
1297

1298
        if ($this->pregEscapeChar === []) {
970✔
1299
            if (is_array($this->escapeChar)) {
272✔
1300
                $this->pregEscapeChar = [
×
1301
                    preg_quote($this->escapeChar[0], '/'),
×
1302
                    preg_quote($this->escapeChar[1], '/'),
×
1303
                    $this->escapeChar[0],
×
1304
                    $this->escapeChar[1],
×
1305
                ];
×
1306
            } else {
1307
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
272✔
1308
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
272✔
1309
            }
1310
        }
1311

1312
        foreach ($this->reservedIdentifiers as $id) {
970✔
1313
            /** @psalm-suppress NoValue I don't know why ERROR. */
1314
            if (str_contains($item, '.' . $id)) {
970✔
1315
                return preg_replace(
3✔
1316
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1317
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1318
                    $item
3✔
1319
                );
3✔
1320
            }
1321
        }
1322

1323
        /** @psalm-suppress NoValue I don't know why ERROR. */
1324
        return preg_replace(
968✔
1325
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
968✔
1326
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
968✔
1327
            $item
968✔
1328
        );
968✔
1329
    }
1330

1331
    /**
1332
     * Prepends a database prefix if one exists in configuration
1333
     *
1334
     * @throws DatabaseException
1335
     */
1336
    public function prefixTable(string $table = ''): string
1337
    {
1338
        if ($table === '') {
3✔
1339
            throw new DatabaseException('A table name is required for that operation.');
×
1340
        }
1341

1342
        return $this->DBPrefix . $table;
3✔
1343
    }
1344

1345
    /**
1346
     * Returns the total number of rows affected by this query.
1347
     */
1348
    abstract public function affectedRows(): int;
1349

1350
    /**
1351
     * "Smart" Escape String
1352
     *
1353
     * Escapes data based on type.
1354
     * Sets boolean and null types
1355
     *
1356
     * @param array|bool|float|int|object|string|null $str
1357
     *
1358
     * @return         array|float|int|string
1359
     * @phpstan-return ($str is array ? array : float|int|string)
1360
     */
1361
    public function escape($str)
1362
    {
1363
        if (is_array($str)) {
824✔
1364
            return array_map($this->escape(...), $str);
655✔
1365
        }
1366

1367
        if ($str instanceof Stringable) {
824✔
1368
            if ($str instanceof RawSql) {
13✔
1369
                return $str->__toString();
12✔
1370
            }
1371

1372
            $str = (string) $str;
1✔
1373
        }
1374

1375
        if (is_string($str)) {
821✔
1376
            return "'" . $this->escapeString($str) . "'";
771✔
1377
        }
1378

1379
        if (is_bool($str)) {
742✔
1380
            return ($str === false) ? 0 : 1;
6✔
1381
        }
1382

1383
        return $str ?? 'NULL';
740✔
1384
    }
1385

1386
    /**
1387
     * Escape String
1388
     *
1389
     * @param list<string|Stringable>|string|Stringable $str  Input string
1390
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1391
     *
1392
     * @return list<string>|string
1393
     */
1394
    public function escapeString($str, bool $like = false)
1395
    {
1396
        if (is_array($str)) {
771✔
1397
            foreach ($str as $key => $val) {
×
1398
                $str[$key] = $this->escapeString($val, $like);
×
1399
            }
1400

1401
            return $str;
×
1402
        }
1403

1404
        if ($str instanceof Stringable) {
771✔
1405
            if ($str instanceof RawSql) {
2✔
1406
                return $str->__toString();
×
1407
            }
1408

1409
            $str = (string) $str;
2✔
1410
        }
1411

1412
        $str = $this->_escapeString($str);
771✔
1413

1414
        // escape LIKE condition wildcards
1415
        if ($like) {
771✔
1416
            return str_replace(
2✔
1417
                [
2✔
1418
                    $this->likeEscapeChar,
2✔
1419
                    '%',
2✔
1420
                    '_',
2✔
1421
                ],
2✔
1422
                [
2✔
1423
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1424
                    $this->likeEscapeChar . '%',
2✔
1425
                    $this->likeEscapeChar . '_',
2✔
1426
                ],
2✔
1427
                $str
2✔
1428
            );
2✔
1429
        }
1430

1431
        return $str;
771✔
1432
    }
1433

1434
    /**
1435
     * Escape LIKE String
1436
     *
1437
     * Calls the individual driver for platform
1438
     * specific escaping for LIKE conditions
1439
     *
1440
     * @param list<string|Stringable>|string|Stringable $str
1441
     *
1442
     * @return list<string>|string
1443
     */
1444
    public function escapeLikeString($str)
1445
    {
1446
        return $this->escapeString($str, true);
2✔
1447
    }
1448

1449
    /**
1450
     * Platform independent string escape.
1451
     *
1452
     * Will likely be overridden in child classes.
1453
     */
1454
    protected function _escapeString(string $str): string
1455
    {
1456
        return str_replace("'", "''", remove_invisible_characters($str, false));
725✔
1457
    }
1458

1459
    /**
1460
     * This function enables you to call PHP database functions that are not natively included
1461
     * in CodeIgniter, in a platform independent manner.
1462
     *
1463
     * @param array ...$params
1464
     *
1465
     * @throws DatabaseException
1466
     */
1467
    public function callFunction(string $functionName, ...$params): bool
1468
    {
1469
        $driver = $this->getDriverFunctionPrefix();
×
1470

1471
        if (! str_contains($driver, $functionName)) {
×
1472
            $functionName = $driver . $functionName;
×
1473
        }
1474

1475
        if (! function_exists($functionName)) {
×
1476
            if ($this->DBDebug) {
×
1477
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1478
            }
1479

1480
            return false;
×
1481
        }
1482

1483
        return $functionName(...$params);
×
1484
    }
1485

1486
    /**
1487
     * Get the prefix of the function to access the DB.
1488
     */
1489
    protected function getDriverFunctionPrefix(): string
1490
    {
1491
        return strtolower($this->DBDriver) . '_';
×
1492
    }
1493

1494
    // --------------------------------------------------------------------
1495
    // META Methods
1496
    // --------------------------------------------------------------------
1497

1498
    /**
1499
     * Returns an array of table names
1500
     *
1501
     * @return false|list<string>
1502
     *
1503
     * @throws DatabaseException
1504
     */
1505
    public function listTables(bool $constrainByPrefix = false)
1506
    {
1507
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
684✔
1508
            return $constrainByPrefix
677✔
1509
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1510
                : $this->dataCache['table_names'];
677✔
1511
        }
1512

1513
        $sql = $this->_listTables($constrainByPrefix);
52✔
1514

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

1520
            return false;
×
1521
        }
1522

1523
        $this->dataCache['table_names'] = [];
52✔
1524

1525
        $query = $this->query($sql);
52✔
1526

1527
        foreach ($query->getResultArray() as $row) {
52✔
1528
            /** @var string $table */
1529
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
49✔
1530

1531
            $this->dataCache['table_names'][] = $table;
49✔
1532
        }
1533

1534
        return $this->dataCache['table_names'];
52✔
1535
    }
1536

1537
    /**
1538
     * Determine if a particular table exists
1539
     *
1540
     * @param bool $cached Whether to use data cache
1541
     */
1542
    public function tableExists(string $tableName, bool $cached = true): bool
1543
    {
1544
        if ($cached) {
678✔
1545
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
677✔
1546
        }
1547

1548
        if (false === ($sql = $this->_listTables(false, $tableName))) {
634✔
1549
            if ($this->DBDebug) {
×
1550
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1551
            }
1552

1553
            return false;
×
1554
        }
1555

1556
        $tableExists = $this->query($sql)->getResultArray() !== [];
634✔
1557

1558
        // if cache has been built already
1559
        if (! empty($this->dataCache['table_names'])) {
634✔
1560
            $key = array_search(
630✔
1561
                strtolower($tableName),
630✔
1562
                array_map(strtolower(...), $this->dataCache['table_names']),
630✔
1563
                true
630✔
1564
            );
630✔
1565

1566
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1567
            // OR if table does exist but is not found in cache
1568
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
630✔
1569
                $this->resetDataCache();
1✔
1570
            }
1571
        }
1572

1573
        return $tableExists;
634✔
1574
    }
1575

1576
    /**
1577
     * Fetch Field Names
1578
     *
1579
     * @param string|TableName $tableName
1580
     *
1581
     * @return false|list<string>
1582
     *
1583
     * @throws DatabaseException
1584
     */
1585
    public function getFieldNames($tableName)
1586
    {
1587
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
12✔
1588

1589
        // Is there a cached result?
1590
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1591
            return $this->dataCache['field_names'][$table];
7✔
1592
        }
1593

1594
        if (empty($this->connID)) {
8✔
1595
            $this->initialize();
×
1596
        }
1597

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

1603
            return false;
×
1604
        }
1605

1606
        $query = $this->query($sql);
8✔
1607

1608
        $this->dataCache['field_names'][$table] = [];
8✔
1609

1610
        foreach ($query->getResultArray() as $row) {
8✔
1611
            // Do we know from where to get the column's name?
1612
            if (! isset($key)) {
8✔
1613
                if (isset($row['column_name'])) {
8✔
1614
                    $key = 'column_name';
8✔
1615
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1616
                    $key = 'COLUMN_NAME';
8✔
1617
                } else {
1618
                    // We have no other choice but to just get the first element's key.
1619
                    $key = key($row);
8✔
1620
                }
1621
            }
1622

1623
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1624
        }
1625

1626
        return $this->dataCache['field_names'][$table];
8✔
1627
    }
1628

1629
    /**
1630
     * Determine if a particular field exists
1631
     */
1632
    public function fieldExists(string $fieldName, string $tableName): bool
1633
    {
1634
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1635
    }
1636

1637
    /**
1638
     * Returns an object with field data
1639
     *
1640
     * @return list<stdClass>
1641
     */
1642
    public function getFieldData(string $table)
1643
    {
1644
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
138✔
1645
    }
1646

1647
    /**
1648
     * Returns an object with key data
1649
     *
1650
     * @return array<string, stdClass>
1651
     */
1652
    public function getIndexData(string $table)
1653
    {
1654
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
153✔
1655
    }
1656

1657
    /**
1658
     * Returns an object with foreign key data
1659
     *
1660
     * @return array<string, stdClass>
1661
     */
1662
    public function getForeignKeyData(string $table)
1663
    {
1664
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
34✔
1665
    }
1666

1667
    /**
1668
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1669
     *
1670
     * @return array<string, stdClass>
1671
     *
1672
     * array[
1673
     *    {constraint_name} =>
1674
     *        stdClass[
1675
     *            'constraint_name'     => string,
1676
     *            'table_name'          => string,
1677
     *            'column_name'         => string[],
1678
     *            'foreign_table_name'  => string,
1679
     *            'foreign_column_name' => string[],
1680
     *            'on_delete'           => string,
1681
     *            'on_update'           => string,
1682
     *            'match'               => string
1683
     *        ]
1684
     * ]
1685
     */
1686
    protected function foreignKeyDataToObjects(array $data)
1687
    {
1688
        $retVal = [];
34✔
1689

1690
        foreach ($data as $row) {
34✔
1691
            $name = $row['constraint_name'];
12✔
1692

1693
            // for sqlite generate name
1694
            if ($name === null) {
12✔
1695
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1696
            }
1697

1698
            $obj                      = new stdClass();
12✔
1699
            $obj->constraint_name     = $name;
12✔
1700
            $obj->table_name          = $row['table_name'];
12✔
1701
            $obj->column_name         = $row['column_name'];
12✔
1702
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1703
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1704
            $obj->on_delete           = $row['on_delete'];
12✔
1705
            $obj->on_update           = $row['on_update'];
12✔
1706
            $obj->match               = $row['match'];
12✔
1707

1708
            $retVal[$name] = $obj;
12✔
1709
        }
1710

1711
        return $retVal;
34✔
1712
    }
1713

1714
    /**
1715
     * Disables foreign key checks temporarily.
1716
     *
1717
     * @return bool
1718
     */
1719
    public function disableForeignKeyChecks()
1720
    {
1721
        $sql = $this->_disableForeignKeyChecks();
647✔
1722

1723
        if ($sql === '') {
647✔
1724
            // The feature is not supported.
1725
            return false;
×
1726
        }
1727

1728
        return $this->query($sql);
647✔
1729
    }
1730

1731
    /**
1732
     * Enables foreign key checks temporarily.
1733
     *
1734
     * @return bool
1735
     */
1736
    public function enableForeignKeyChecks()
1737
    {
1738
        $sql = $this->_enableForeignKeyChecks();
723✔
1739

1740
        if ($sql === '') {
723✔
1741
            // The feature is not supported.
1742
            return false;
×
1743
        }
1744

1745
        return $this->query($sql);
723✔
1746
    }
1747

1748
    /**
1749
     * Allows the engine to be set into a mode where queries are not
1750
     * actually executed, but they are still generated, timed, etc.
1751
     *
1752
     * This is primarily used by the prepared query functionality.
1753
     *
1754
     * @return $this
1755
     */
1756
    public function pretend(bool $pretend = true)
1757
    {
1758
        $this->pretend = $pretend;
14✔
1759

1760
        return $this;
14✔
1761
    }
1762

1763
    /**
1764
     * Empties our data cache. Especially helpful during testing.
1765
     *
1766
     * @return $this
1767
     */
1768
    public function resetDataCache()
1769
    {
1770
        $this->dataCache = [];
33✔
1771

1772
        return $this;
33✔
1773
    }
1774

1775
    /**
1776
     * Determines if the statement is a write-type query or not.
1777
     *
1778
     * @param string $sql
1779
     */
1780
    public function isWriteType($sql): bool
1781
    {
1782
        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);
749✔
1783
    }
1784

1785
    /**
1786
     * Returns the last error code and message.
1787
     *
1788
     * Must return an array with keys 'code' and 'message':
1789
     *
1790
     * @return         array<string, int|string|null>
1791
     * @phpstan-return array{code: int|string|null, message: string|null}
1792
     */
1793
    abstract public function error(): array;
1794

1795
    /**
1796
     * Insert ID
1797
     *
1798
     * @return int|string
1799
     */
1800
    abstract public function insertID();
1801

1802
    /**
1803
     * Generates the SQL for listing tables in a platform-dependent manner.
1804
     *
1805
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1806
     *
1807
     * @return false|string
1808
     */
1809
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1810

1811
    /**
1812
     * Generates a platform-specific query string so that the column names can be fetched.
1813
     *
1814
     * @param string|TableName $table
1815
     *
1816
     * @return false|string
1817
     */
1818
    abstract protected function _listColumns($table = '');
1819

1820
    /**
1821
     * Platform-specific field data information.
1822
     *
1823
     * @see getFieldData()
1824
     *
1825
     * @return list<stdClass>
1826
     */
1827
    abstract protected function _fieldData(string $table): array;
1828

1829
    /**
1830
     * Platform-specific index data.
1831
     *
1832
     * @see    getIndexData()
1833
     *
1834
     * @return array<string, stdClass>
1835
     */
1836
    abstract protected function _indexData(string $table): array;
1837

1838
    /**
1839
     * Platform-specific foreign keys data.
1840
     *
1841
     * @see    getForeignKeyData()
1842
     *
1843
     * @return array<string, stdClass>
1844
     */
1845
    abstract protected function _foreignKeyData(string $table): array;
1846

1847
    /**
1848
     * Platform-specific SQL statement to disable foreign key checks.
1849
     *
1850
     * If this feature is not supported, return empty string.
1851
     *
1852
     * @TODO This method should be moved to an interface that represents foreign key support.
1853
     *
1854
     * @return string
1855
     *
1856
     * @see disableForeignKeyChecks()
1857
     */
1858
    protected function _disableForeignKeyChecks()
1859
    {
1860
        return '';
×
1861
    }
1862

1863
    /**
1864
     * Platform-specific SQL statement to enable foreign key checks.
1865
     *
1866
     * If this feature is not supported, return empty string.
1867
     *
1868
     * @TODO This method should be moved to an interface that represents foreign key support.
1869
     *
1870
     * @return string
1871
     *
1872
     * @see enableForeignKeyChecks()
1873
     */
1874
    protected function _enableForeignKeyChecks()
1875
    {
1876
        return '';
×
1877
    }
1878

1879
    /**
1880
     * Accessor for properties if they exist.
1881
     *
1882
     * @return array|bool|float|int|object|resource|string|null
1883
     */
1884
    public function __get(string $key)
1885
    {
1886
        if (property_exists($this, $key)) {
937✔
1887
            return $this->{$key};
936✔
1888
        }
1889

1890
        return null;
1✔
1891
    }
1892

1893
    /**
1894
     * Checker for properties existence.
1895
     */
1896
    public function __isset(string $key): bool
1897
    {
1898
        return property_exists($this, $key);
217✔
1899
    }
1900
}
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