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

codeigniter4 / CodeIgniter4 / 8678307574

14 Apr 2024 04:14AM UTC coverage: 84.44%. Remained the same
8678307574

push

github

web-flow
Merge pull request #8783 from codeigniter4/develop

4.5.1 Ready code

24 of 32 new or added lines in 12 files covered. (75.0%)

164 existing lines in 12 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

85.39
/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'])) {
357✔
373
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
78✔
374
            unset($params['dateFormat']);
78✔
375
        }
376

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

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

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

389
        if ($this->failover !== []) {
357✔
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) {
691✔
413
            return;
662✔
414
        }
415

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

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

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

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

448
                    // If a connection is made break the foreach loop
449
                    if ($this->connID) {
2✔
450
                        break;
2✔
451
                    }
452
                }
453
            }
454

455
            // We still don't have a connection?
456
            if (! $this->connID) {
3✔
457
                throw new DatabaseException(sprintf(
1✔
458
                    'Unable to connect to the database.%s%s',
1✔
459
                    PHP_EOL,
1✔
460
                    implode(PHP_EOL, $connectionErrors)
1✔
461
                ));
1✔
462
            }
463
        }
464

465
        $this->connectDuration = microtime(true) - $this->connectTime;
35✔
466
    }
467

468
    /**
469
     * Close the database connection.
470
     */
471
    public function close()
472
    {
473
        if ($this->connID) {
1✔
474
            $this->_close();
1✔
475
            $this->connID = false;
1✔
476
        }
477
    }
478

479
    /**
480
     * Platform dependent way method for closing the connection.
481
     *
482
     * @return mixed
483
     */
484
    abstract protected function _close();
485

486
    /**
487
     * Create a persistent database connection.
488
     *
489
     * @return         false|object|resource
490
     * @phpstan-return false|TConnection
491
     */
492
    public function persistentConnect()
493
    {
UNCOV
494
        return $this->connect(true);
×
495
    }
496

497
    /**
498
     * Returns the actual connection object. If both a 'read' and 'write'
499
     * connection has been specified, you can pass either term in to
500
     * get that connection. If you pass either alias in and only a single
501
     * connection is present, it must return the sole connection.
502
     *
503
     * @return         false|object|resource
504
     * @phpstan-return TConnection
505
     */
506
    public function getConnection(?string $alias = null)
507
    {
508
        // @todo work with read/write connections
509
        return $this->connID;
2✔
510
    }
511

512
    /**
513
     * Returns the name of the current database being used.
514
     */
515
    public function getDatabase(): string
516
    {
517
        return empty($this->database) ? '' : $this->database;
648✔
518
    }
519

520
    /**
521
     * Set DB Prefix
522
     *
523
     * Set's the DB Prefix to something new without needing to reconnect
524
     *
525
     * @param string $prefix The prefix
526
     */
527
    public function setPrefix(string $prefix = ''): string
528
    {
529
        return $this->DBPrefix = $prefix;
13✔
530
    }
531

532
    /**
533
     * Returns the database prefix.
534
     */
535
    public function getPrefix(): string
536
    {
537
        return $this->DBPrefix;
12✔
538
    }
539

540
    /**
541
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
542
     */
543
    public function getPlatform(): string
544
    {
545
        return $this->DBDriver;
22✔
546
    }
547

548
    /**
549
     * Sets the Table Aliases to use. These are typically
550
     * collected during use of the Builder, and set here
551
     * so queries are built correctly.
552
     *
553
     * @return $this
554
     */
555
    public function setAliasedTables(array $aliases)
556
    {
557
        $this->aliasedTables = $aliases;
826✔
558

559
        return $this;
826✔
560
    }
561

562
    /**
563
     * Add a table alias to our list.
564
     *
565
     * @return $this
566
     */
567
    public function addTableAlias(string $table)
568
    {
569
        if (! in_array($table, $this->aliasedTables, true)) {
23✔
570
            $this->aliasedTables[] = $table;
23✔
571
        }
572

573
        return $this;
23✔
574
    }
575

576
    /**
577
     * Executes the query against the database.
578
     *
579
     * @return         false|object|resource
580
     * @phpstan-return false|TResult
581
     */
582
    abstract protected function execute(string $sql);
583

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

603
        if (empty($this->connID)) {
689✔
604
            $this->initialize();
3✔
605
        }
606

607
        /**
608
         * @var Query $query
609
         */
610
        $query = new $queryClass($this);
689✔
611

612
        $query->setQuery($sql, $binds, $setEscapeFlags);
689✔
613

614
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
689✔
UNCOV
615
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
616
        }
617

618
        $startTime = microtime(true);
689✔
619

620
        // Always save the last query so we can use
621
        // the getLastQuery() method.
622
        $this->lastQuery = $query;
689✔
623

624
        // If $pretend is true, then we just want to return
625
        // the actual query object here. There won't be
626
        // any results to return.
627
        if ($this->pretend) {
689✔
628
            $query->setDuration($startTime);
8✔
629

630
            return $query;
8✔
631
        }
632

633
        // Run the query for real
634
        try {
635
            $exception      = null;
689✔
636
            $this->resultID = $this->simpleQuery($query->getQuery());
689✔
637
        } catch (DatabaseException $exception) {
10✔
638
            $this->resultID = false;
10✔
639
        }
640

641
        if ($this->resultID === false) {
689✔
642
            $query->setDuration($startTime, $startTime);
42✔
643

644
            // This will trigger a rollback if transactions are being used
645
            if ($this->transDepth !== 0) {
42✔
646
                $this->transStatus = false;
14✔
647
            }
648

649
            if (
650
                $this->DBDebug
42✔
651
                && (
652
                    // Not in transactions
653
                    $this->transDepth === 0
42✔
654
                    // In transactions, do not throw exception by default.
42✔
655
                    || $this->transException
42✔
656
                )
657
            ) {
658
                // We call this function in order to roll-back queries
659
                // if transactions are enabled. If we don't call this here
660
                // the error message will trigger an exit, causing the
661
                // transactions to remain in limbo.
662
                while ($this->transDepth !== 0) {
23✔
663
                    $transDepth = $this->transDepth;
2✔
664
                    $this->transComplete();
2✔
665

666
                    if ($transDepth === $this->transDepth) {
2✔
UNCOV
667
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
668
                        break;
×
669
                    }
670
                }
671

672
                // Let others do something with this query.
673
                Events::trigger('DBQuery', $query);
23✔
674

675
                if ($exception !== null) {
23✔
676
                    throw new DatabaseException(
6✔
677
                        $exception->getMessage(),
6✔
678
                        $exception->getCode(),
6✔
679
                        $exception
6✔
680
                    );
6✔
681
                }
682

683
                return false;
17✔
684
            }
685

686
            // Let others do something with this query.
687
            Events::trigger('DBQuery', $query);
19✔
688

689
            return false;
19✔
690
        }
691

692
        $query->setDuration($startTime);
689✔
693

694
        // Let others do something with this query
695
        Events::trigger('DBQuery', $query);
689✔
696

697
        // resultID is not false, so it must be successful
698
        if ($this->isWriteType($sql)) {
689✔
699
            return true;
656✔
700
        }
701

702
        // query is not write-type, so it must be read-type query; return QueryResult
703
        $resultClass = str_replace('Connection', 'Result', static::class);
689✔
704

705
        return new $resultClass($this->connID, $this->resultID);
689✔
706
    }
707

708
    /**
709
     * Performs a basic query against the database. No binding or caching
710
     * is performed, nor are transactions handled. Simply takes a raw
711
     * query string and returns the database-specific result id.
712
     *
713
     * @return         false|object|resource
714
     * @phpstan-return false|TResult
715
     */
716
    public function simpleQuery(string $sql)
717
    {
718
        if (empty($this->connID)) {
695✔
719
            $this->initialize();
6✔
720
        }
721

722
        return $this->execute($sql);
695✔
723
    }
724

725
    /**
726
     * Disable Transactions
727
     *
728
     * This permits transactions to be disabled at run-time.
729
     */
730
    public function transOff()
731
    {
UNCOV
732
        $this->transEnabled = false;
×
733
    }
734

735
    /**
736
     * Enable/disable Transaction Strict Mode
737
     *
738
     * When strict mode is enabled, if you are running multiple groups of
739
     * transactions, if one group fails all subsequent groups will be
740
     * rolled back.
741
     *
742
     * If strict mode is disabled, each group is treated autonomously,
743
     * meaning a failure of one group will not affect any others
744
     *
745
     * @param bool $mode = true
746
     *
747
     * @return $this
748
     */
749
    public function transStrict(bool $mode = true)
750
    {
751
        $this->transStrict = $mode;
3✔
752

753
        return $this;
3✔
754
    }
755

756
    /**
757
     * Start Transaction
758
     */
759
    public function transStart(bool $testMode = false): bool
760
    {
761
        if (! $this->transEnabled) {
42✔
UNCOV
762
            return false;
×
763
        }
764

765
        return $this->transBegin($testMode);
42✔
766
    }
767

768
    /**
769
     * If set to true, exceptions are thrown during transactions.
770
     *
771
     * @return $this
772
     */
773
    public function transException(bool $transExcetion)
774
    {
775
        $this->transException = $transExcetion;
3✔
776

777
        return $this;
3✔
778
    }
779

780
    /**
781
     * Complete Transaction
782
     */
783
    public function transComplete(): bool
784
    {
785
        if (! $this->transEnabled) {
42✔
UNCOV
786
            return false;
×
787
        }
788

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

793
            // If we are NOT running in strict mode, we will reset
794
            // the _trans_status flag so that subsequent groups of
795
            // transactions will be permitted.
796
            if ($this->transStrict === false) {
12✔
797
                $this->transStatus = true;
3✔
798
            }
799

800
            return false;
12✔
801
        }
802

803
        return $this->transCommit();
33✔
804
    }
805

806
    /**
807
     * Lets you retrieve the transaction flag to determine if it has failed
808
     */
809
    public function transStatus(): bool
810
    {
811
        return $this->transStatus;
11✔
812
    }
813

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

823
        // When transactions are nested we only begin/commit/rollback the outermost ones
824
        if ($this->transDepth > 0) {
44✔
UNCOV
825
            $this->transDepth++;
×
826

UNCOV
827
            return true;
×
828
        }
829

830
        if (empty($this->connID)) {
44✔
UNCOV
831
            $this->initialize();
×
832
        }
833

834
        // Reset the transaction failure flag.
835
        // If the $test_mode flag is set to TRUE transactions will be rolled back
836
        // even if the queries produce a successful result.
837
        $this->transFailure = ($testMode === true);
44✔
838

839
        if ($this->_transBegin()) {
44✔
840
            $this->transDepth++;
44✔
841

842
            return true;
44✔
843
        }
844

UNCOV
845
        return false;
×
846
    }
847

848
    /**
849
     * Commit Transaction
850
     */
851
    public function transCommit(): bool
852
    {
853
        if (! $this->transEnabled || $this->transDepth === 0) {
33✔
UNCOV
854
            return false;
×
855
        }
856

857
        // When transactions are nested we only begin/commit/rollback the outermost ones
858
        if ($this->transDepth > 1 || $this->_transCommit()) {
33✔
859
            $this->transDepth--;
33✔
860

861
            return true;
33✔
862
        }
863

UNCOV
864
        return false;
×
865
    }
866

867
    /**
868
     * Rollback Transaction
869
     */
870
    public function transRollback(): bool
871
    {
872
        if (! $this->transEnabled || $this->transDepth === 0) {
14✔
UNCOV
873
            return false;
×
874
        }
875

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

880
            return true;
14✔
881
        }
882

UNCOV
883
        return false;
×
884
    }
885

886
    /**
887
     * Begin Transaction
888
     */
889
    abstract protected function _transBegin(): bool;
890

891
    /**
892
     * Commit Transaction
893
     */
894
    abstract protected function _transCommit(): bool;
895

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

901
    /**
902
     * Returns a non-shared new instance of the query builder for this connection.
903
     *
904
     * @param array|string $tableName
905
     *
906
     * @return BaseBuilder
907
     *
908
     * @throws DatabaseException
909
     */
910
    public function table($tableName)
911
    {
912
        if (empty($tableName)) {
778✔
UNCOV
913
            throw new DatabaseException('You must set the database table to be used with your query.');
×
914
        }
915

916
        $className = str_replace('Connection', 'Builder', static::class);
778✔
917

918
        return new $className($tableName, $this);
778✔
919
    }
920

921
    /**
922
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
923
     */
924
    public function newQuery(): BaseBuilder
925
    {
926
        // save table aliases
927
        $tempAliases         = $this->aliasedTables;
14✔
928
        $builder             = $this->table(',')->from([], true);
14✔
929
        $this->aliasedTables = $tempAliases;
14✔
930

931
        return $builder;
14✔
932
    }
933

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

956
        $this->pretend();
12✔
957

958
        $sql = $func($this);
12✔
959

960
        $this->pretend(false);
12✔
961

962
        if ($sql instanceof QueryInterface) {
12✔
963
            $sql = $sql->getOriginalQuery();
12✔
964
        }
965

966
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
12✔
967
        /** @var BasePreparedQuery $class */
968
        $class = new $class($this);
12✔
969

970
        return $class->prepare($sql, $options);
12✔
971
    }
972

973
    /**
974
     * Returns the last query's statement object.
975
     *
976
     * @return Query
977
     */
978
    public function getLastQuery()
979
    {
980
        return $this->lastQuery;
11✔
981
    }
982

983
    /**
984
     * Returns a string representation of the last query's statement object.
985
     */
986
    public function showLastQuery(): string
987
    {
UNCOV
988
        return (string) $this->lastQuery;
×
989
    }
990

991
    /**
992
     * Returns the time we started to connect to this database in
993
     * seconds with microseconds.
994
     *
995
     * Used by the Debug Toolbar's timeline.
996
     */
997
    public function getConnectStart(): ?float
998
    {
999
        return $this->connectTime;
1✔
1000
    }
1001

1002
    /**
1003
     * Returns the number of seconds with microseconds that it took
1004
     * to connect to the database.
1005
     *
1006
     * Used by the Debug Toolbar's timeline.
1007
     */
1008
    public function getConnectDuration(int $decimals = 6): string
1009
    {
1010
        return number_format($this->connectDuration, $decimals);
2✔
1011
    }
1012

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

1047
        if (is_array($item)) {
931✔
1048
            $escapedArray = [];
1✔
1049

1050
            foreach ($item as $k => $v) {
1✔
1051
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1052
            }
1053

1054
            return $escapedArray;
1✔
1055
        }
1056

1057
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1058
        $item = (string) $item;
931✔
1059

1060
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1061
        // If a parenthesis is found we know that we do not need to
1062
        // escape the data or add a prefix. There's probably a more graceful
1063
        // way to deal with this, but I'm not thinking of it
1064
        //
1065
        // Added exception for single quotes as well, we don't want to alter
1066
        // literal strings.
1067
        if (strcspn($item, "()'") !== strlen($item)) {
931✔
1068
            /** @psalm-suppress NoValue I don't know why ERROR. */
1069
            return $item;
663✔
1070
        }
1071

1072
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1073
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
920✔
1074
            /** @psalm-suppress NoValue I don't know why ERROR. */
1075
            return $item;
88✔
1076
        }
1077

1078
        // Convert tabs or multiple spaces into single spaces
1079
        /** @psalm-suppress NoValue I don't know why ERROR. */
1080
        $item = preg_replace('/\s+/', ' ', trim($item));
919✔
1081

1082
        // If the item has an alias declaration we remove it and set it aside.
1083
        // Note: strripos() is used in order to support spaces in table names
1084
        if ($offset = strripos($item, ' AS ')) {
919✔
1085
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1086
            $item  = substr($item, 0, $offset);
11✔
1087
        } elseif ($offset = strrpos($item, ' ')) {
914✔
1088
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
11✔
1089
            $item  = substr($item, 0, $offset);
11✔
1090
        } else {
1091
            $alias = '';
909✔
1092
        }
1093

1094
        // Break the string apart if it contains periods, then insert the table prefix
1095
        // in the correct location, assuming the period doesn't indicate that we're dealing
1096
        // with an alias. While we're at it, we will escape the components
1097
        if (str_contains($item, '.')) {
919✔
1098
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
124✔
1099
        }
1100

1101
        // In some cases, especially 'from', we end up running through
1102
        // protect_identifiers twice. This algorithm won't work when
1103
        // it contains the escapeChar so strip it out.
1104
        $item = trim($item, $this->escapeChar);
913✔
1105

1106
        // Is there a table prefix? If not, no need to insert it
1107
        if ($this->DBPrefix !== '') {
913✔
1108
            // Verify table prefix and replace if necessary
1109
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
696✔
UNCOV
1110
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1111
            }
1112
            // Do we prefix an item with no segments?
1113
            elseif ($prefixSingle === true && ! str_starts_with($item, $this->DBPrefix)) {
696✔
1114
                $item = $this->DBPrefix . $item;
689✔
1115
            }
1116
        }
1117

1118
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
913✔
1119
            $item = $this->escapeIdentifiers($item);
911✔
1120
        }
1121

1122
        return $item . $alias;
913✔
1123
    }
1124

1125
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1126
    {
1127
        $parts = explode('.', $item);
124✔
1128

1129
        // Does the first segment of the exploded item match
1130
        // one of the aliases previously identified? If so,
1131
        // we have nothing more to do other than escape the item
1132
        //
1133
        // NOTE: The ! empty() condition prevents this method
1134
        // from breaking when QB isn't enabled.
1135
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
124✔
1136
            if ($protectIdentifiers === true) {
10✔
1137
                foreach ($parts as $key => $val) {
10✔
1138
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
10✔
1139
                        $parts[$key] = $this->escapeIdentifiers($val);
10✔
1140
                    }
1141
                }
1142

1143
                $item = implode('.', $parts);
10✔
1144
            }
1145

1146
            return $item . $alias;
10✔
1147
        }
1148

1149
        // Is there a table prefix defined in the config file? If not, no need to do anything
1150
        if ($this->DBPrefix !== '') {
118✔
1151
            // We now add the table prefix based on some logic.
1152
            // Do we have 4 segments (hostname.database.table.column)?
1153
            // If so, we add the table prefix to the column name in the 3rd segment.
1154
            if (isset($parts[3])) {
113✔
UNCOV
1155
                $i = 2;
×
1156
            }
1157
            // Do we have 3 segments (database.table.column)?
1158
            // If so, we add the table prefix to the column name in 2nd position
1159
            elseif (isset($parts[2])) {
113✔
UNCOV
1160
                $i = 1;
×
1161
            }
1162
            // Do we have 2 segments (table.column)?
1163
            // If so, we add the table prefix to the column name in 1st segment
1164
            else {
1165
                $i = 0;
113✔
1166
            }
1167

1168
            // This flag is set when the supplied $item does not contain a field name.
1169
            // This can happen when this function is being called from a JOIN.
1170
            if ($fieldExists === false) {
113✔
UNCOV
1171
                $i++;
×
1172
            }
1173

1174
            // Verify table prefix and replace if necessary
1175
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
113✔
UNCOV
1176
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1177
            }
1178
            // We only add the table prefix if it does not already exist
1179
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
113✔
1180
                $parts[$i] = $this->DBPrefix . $parts[$i];
113✔
1181
            }
1182

1183
            // Put the parts back together
1184
            $item = implode('.', $parts);
113✔
1185
        }
1186

1187
        if ($protectIdentifiers === true) {
118✔
1188
            $item = $this->escapeIdentifiers($item);
118✔
1189
        }
1190

1191
        return $item . $alias;
118✔
1192
    }
1193

1194
    /**
1195
     * Escape the SQL Identifier
1196
     *
1197
     * This function escapes single identifier.
1198
     *
1199
     * @param non-empty-string $item
1200
     */
1201
    public function escapeIdentifier(string $item): string
1202
    {
1203
        return $this->escapeChar
591✔
1204
            . str_replace(
591✔
1205
                $this->escapeChar,
591✔
1206
                $this->escapeChar . $this->escapeChar,
591✔
1207
                $item
591✔
1208
            )
591✔
1209
            . $this->escapeChar;
591✔
1210
    }
1211

1212
    /**
1213
     * Escape the SQL Identifiers
1214
     *
1215
     * This function escapes column and table names
1216
     *
1217
     * @param array|string $item
1218
     *
1219
     * @return         array|string
1220
     * @phpstan-return ($item is array ? array : string)
1221
     */
1222
    public function escapeIdentifiers($item)
1223
    {
1224
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
927✔
1225
            return $item;
5✔
1226
        }
1227

1228
        if (is_array($item)) {
926✔
1229
            foreach ($item as $key => $value) {
609✔
1230
                $item[$key] = $this->escapeIdentifiers($value);
609✔
1231
            }
1232

1233
            return $item;
609✔
1234
        }
1235

1236
        // Avoid breaking functions and literal values inside queries
1237
        if (ctype_digit($item)
926✔
1238
            || $item[0] === "'"
925✔
1239
            || ($this->escapeChar !== '"' && $item[0] === '"')
925✔
1240
            || str_contains($item, '(')) {
926✔
1241
            return $item;
39✔
1242
        }
1243

1244
        if ($this->pregEscapeChar === []) {
925✔
1245
            if (is_array($this->escapeChar)) {
256✔
UNCOV
1246
                $this->pregEscapeChar = [
×
1247
                    preg_quote($this->escapeChar[0], '/'),
×
1248
                    preg_quote($this->escapeChar[1], '/'),
×
1249
                    $this->escapeChar[0],
×
1250
                    $this->escapeChar[1],
×
1251
                ];
×
1252
            } else {
1253
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
256✔
1254
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
256✔
1255
            }
1256
        }
1257

1258
        foreach ($this->reservedIdentifiers as $id) {
925✔
1259
            /** @psalm-suppress NoValue I don't know why ERROR. */
1260
            if (str_contains($item, '.' . $id)) {
925✔
1261
                return preg_replace(
3✔
1262
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1263
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1264
                    $item
3✔
1265
                );
3✔
1266
            }
1267
        }
1268

1269
        /** @psalm-suppress NoValue I don't know why ERROR. */
1270
        return preg_replace(
923✔
1271
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
923✔
1272
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
923✔
1273
            $item
923✔
1274
        );
923✔
1275
    }
1276

1277
    /**
1278
     * Prepends a database prefix if one exists in configuration
1279
     *
1280
     * @throws DatabaseException
1281
     */
1282
    public function prefixTable(string $table = ''): string
1283
    {
1284
        if ($table === '') {
3✔
UNCOV
1285
            throw new DatabaseException('A table name is required for that operation.');
×
1286
        }
1287

1288
        return $this->DBPrefix . $table;
3✔
1289
    }
1290

1291
    /**
1292
     * Returns the total number of rows affected by this query.
1293
     */
1294
    abstract public function affectedRows(): int;
1295

1296
    /**
1297
     * "Smart" Escape String
1298
     *
1299
     * Escapes data based on type.
1300
     * Sets boolean and null types
1301
     *
1302
     * @param array|bool|float|int|object|string|null $str
1303
     *
1304
     * @return         array|float|int|string
1305
     * @phpstan-return ($str is array ? array : float|int|string)
1306
     */
1307
    public function escape($str)
1308
    {
1309
        if (is_array($str)) {
791✔
1310
            return array_map($this->escape(...), $str);
624✔
1311
        }
1312

1313
        if ($str instanceof Stringable) {
791✔
1314
            if ($str instanceof RawSql) {
13✔
1315
                return $str->__toString();
12✔
1316
            }
1317

1318
            $str = (string) $str;
1✔
1319
        }
1320

1321
        if (is_string($str)) {
788✔
1322
            return "'" . $this->escapeString($str) . "'";
739✔
1323
        }
1324

1325
        if (is_bool($str)) {
710✔
1326
            return ($str === false) ? 0 : 1;
6✔
1327
        }
1328

1329
        return $str ?? 'NULL';
708✔
1330
    }
1331

1332
    /**
1333
     * Escape String
1334
     *
1335
     * @param list<string|Stringable>|string|Stringable $str  Input string
1336
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1337
     *
1338
     * @return list<string>|string
1339
     */
1340
    public function escapeString($str, bool $like = false)
1341
    {
1342
        if (is_array($str)) {
739✔
UNCOV
1343
            foreach ($str as $key => $val) {
×
UNCOV
1344
                $str[$key] = $this->escapeString($val, $like);
×
1345
            }
1346

1347
            return $str;
×
1348
        }
1349

1350
        if ($str instanceof Stringable) {
739✔
1351
            if ($str instanceof RawSql) {
2✔
NEW
1352
                return $str->__toString();
×
1353
            }
1354

1355
            $str = (string) $str;
2✔
1356
        }
1357

1358
        $str = $this->_escapeString($str);
739✔
1359

1360
        // escape LIKE condition wildcards
1361
        if ($like === true) {
739✔
1362
            return str_replace(
2✔
1363
                [
2✔
1364
                    $this->likeEscapeChar,
2✔
1365
                    '%',
2✔
1366
                    '_',
2✔
1367
                ],
2✔
1368
                [
2✔
1369
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1370
                    $this->likeEscapeChar . '%',
2✔
1371
                    $this->likeEscapeChar . '_',
2✔
1372
                ],
2✔
1373
                $str
2✔
1374
            );
2✔
1375
        }
1376

1377
        return $str;
739✔
1378
    }
1379

1380
    /**
1381
     * Escape LIKE String
1382
     *
1383
     * Calls the individual driver for platform
1384
     * specific escaping for LIKE conditions
1385
     *
1386
     * @param list<string|Stringable>|string|Stringable $str
1387
     *
1388
     * @return list<string>|string
1389
     */
1390
    public function escapeLikeString($str)
1391
    {
1392
        return $this->escapeString($str, true);
2✔
1393
    }
1394

1395
    /**
1396
     * Platform independent string escape.
1397
     *
1398
     * Will likely be overridden in child classes.
1399
     */
1400
    protected function _escapeString(string $str): string
1401
    {
1402
        return str_replace("'", "''", remove_invisible_characters($str, false));
701✔
1403
    }
1404

1405
    /**
1406
     * This function enables you to call PHP database functions that are not natively included
1407
     * in CodeIgniter, in a platform independent manner.
1408
     *
1409
     * @param array ...$params
1410
     *
1411
     * @throws DatabaseException
1412
     */
1413
    public function callFunction(string $functionName, ...$params): bool
1414
    {
UNCOV
1415
        $driver = $this->getDriverFunctionPrefix();
×
1416

UNCOV
1417
        if (! str_contains($driver, $functionName)) {
×
UNCOV
1418
            $functionName = $driver . $functionName;
×
1419
        }
1420

UNCOV
1421
        if (! function_exists($functionName)) {
×
UNCOV
1422
            if ($this->DBDebug) {
×
UNCOV
1423
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1424
            }
1425

UNCOV
1426
            return false;
×
1427
        }
1428

1429
        return $functionName(...$params);
×
1430
    }
1431

1432
    /**
1433
     * Get the prefix of the function to access the DB.
1434
     */
1435
    protected function getDriverFunctionPrefix(): string
1436
    {
UNCOV
1437
        return strtolower($this->DBDriver) . '_';
×
1438
    }
1439

1440
    // --------------------------------------------------------------------
1441
    // META Methods
1442
    // --------------------------------------------------------------------
1443

1444
    /**
1445
     * Returns an array of table names
1446
     *
1447
     * @return array|false
1448
     *
1449
     * @throws DatabaseException
1450
     */
1451
    public function listTables(bool $constrainByPrefix = false)
1452
    {
1453
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
654✔
1454
            return $constrainByPrefix
647✔
1455
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1456
                : $this->dataCache['table_names'];
647✔
1457
        }
1458

1459
        $sql = $this->_listTables($constrainByPrefix);
49✔
1460

1461
        if ($sql === false) {
49✔
UNCOV
1462
            if ($this->DBDebug) {
×
UNCOV
1463
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1464
            }
1465

UNCOV
1466
            return false;
×
1467
        }
1468

1469
        $this->dataCache['table_names'] = [];
49✔
1470

1471
        $query = $this->query($sql);
49✔
1472

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

1476
            $this->dataCache['table_names'][] = $table;
46✔
1477
        }
1478

1479
        return $this->dataCache['table_names'];
49✔
1480
    }
1481

1482
    /**
1483
     * Determine if a particular table exists
1484
     *
1485
     * @param bool $cached Whether to use data cache
1486
     */
1487
    public function tableExists(string $tableName, bool $cached = true): bool
1488
    {
1489
        if ($cached === true) {
648✔
1490
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
647✔
1491
        }
1492

1493
        if (false === ($sql = $this->_listTables(false, $tableName))) {
604✔
UNCOV
1494
            if ($this->DBDebug) {
×
UNCOV
1495
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1496
            }
1497

UNCOV
1498
            return false;
×
1499
        }
1500

1501
        $tableExists = $this->query($sql)->getResultArray() !== [];
604✔
1502

1503
        // if cache has been built already
1504
        if (! empty($this->dataCache['table_names'])) {
604✔
1505
            $key = array_search(
600✔
1506
                strtolower($tableName),
600✔
1507
                array_map('strtolower', $this->dataCache['table_names']),
600✔
1508
                true
600✔
1509
            );
600✔
1510

1511
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1512
            // OR if table does exist but is not found in cache
1513
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
600✔
1514
                $this->resetDataCache();
1✔
1515
            }
1516
        }
1517

1518
        return $tableExists;
604✔
1519
    }
1520

1521
    /**
1522
     * Fetch Field Names
1523
     *
1524
     * @return array|false
1525
     *
1526
     * @throws DatabaseException
1527
     */
1528
    public function getFieldNames(string $table)
1529
    {
1530
        // Is there a cached result?
1531
        if (isset($this->dataCache['field_names'][$table])) {
12✔
1532
            return $this->dataCache['field_names'][$table];
7✔
1533
        }
1534

1535
        if (empty($this->connID)) {
8✔
UNCOV
1536
            $this->initialize();
×
1537
        }
1538

1539
        if (false === ($sql = $this->_listColumns($table))) {
8✔
UNCOV
1540
            if ($this->DBDebug) {
×
UNCOV
1541
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1542
            }
1543

UNCOV
1544
            return false;
×
1545
        }
1546

1547
        $query = $this->query($sql);
8✔
1548

1549
        $this->dataCache['field_names'][$table] = [];
8✔
1550

1551
        foreach ($query->getResultArray() as $row) {
8✔
1552
            // Do we know from where to get the column's name?
1553
            if (! isset($key)) {
8✔
1554
                if (isset($row['column_name'])) {
8✔
1555
                    $key = 'column_name';
8✔
1556
                } elseif (isset($row['COLUMN_NAME'])) {
8✔
1557
                    $key = 'COLUMN_NAME';
8✔
1558
                } else {
1559
                    // We have no other choice but to just get the first element's key.
1560
                    $key = key($row);
8✔
1561
                }
1562
            }
1563

1564
            $this->dataCache['field_names'][$table][] = $row[$key];
8✔
1565
        }
1566

1567
        return $this->dataCache['field_names'][$table];
8✔
1568
    }
1569

1570
    /**
1571
     * Determine if a particular field exists
1572
     */
1573
    public function fieldExists(string $fieldName, string $tableName): bool
1574
    {
1575
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
1576
    }
1577

1578
    /**
1579
     * Returns an object with field data
1580
     *
1581
     * @return list<stdClass>
1582
     */
1583
    public function getFieldData(string $table)
1584
    {
1585
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
133✔
1586
    }
1587

1588
    /**
1589
     * Returns an object with key data
1590
     *
1591
     * @return array<string, stdClass>
1592
     */
1593
    public function getIndexData(string $table)
1594
    {
1595
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
146✔
1596
    }
1597

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

1608
    /**
1609
     * Converts array of arrays generated by _foreignKeyData() to array of objects
1610
     *
1611
     * @return array<string, stdClass>
1612
     *
1613
     * array[
1614
     *    {constraint_name} =>
1615
     *        stdClass[
1616
     *            'constraint_name'     => string,
1617
     *            'table_name'          => string,
1618
     *            'column_name'         => string[],
1619
     *            'foreign_table_name'  => string,
1620
     *            'foreign_column_name' => string[],
1621
     *            'on_delete'           => string,
1622
     *            'on_update'           => string,
1623
     *            'match'               => string
1624
     *        ]
1625
     * ]
1626
     */
1627
    protected function foreignKeyDataToObjects(array $data)
1628
    {
1629
        $retVal = [];
34✔
1630

1631
        foreach ($data as $row) {
34✔
1632
            $name = $row['constraint_name'];
12✔
1633

1634
            // for sqlite generate name
1635
            if ($name === null) {
12✔
1636
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
1637
            }
1638

1639
            $obj                      = new stdClass();
12✔
1640
            $obj->constraint_name     = $name;
12✔
1641
            $obj->table_name          = $row['table_name'];
12✔
1642
            $obj->column_name         = $row['column_name'];
12✔
1643
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
1644
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
1645
            $obj->on_delete           = $row['on_delete'];
12✔
1646
            $obj->on_update           = $row['on_update'];
12✔
1647
            $obj->match               = $row['match'];
12✔
1648

1649
            $retVal[$name] = $obj;
12✔
1650
        }
1651

1652
        return $retVal;
34✔
1653
    }
1654

1655
    /**
1656
     * Disables foreign key checks temporarily.
1657
     *
1658
     * @return bool
1659
     */
1660
    public function disableForeignKeyChecks()
1661
    {
1662
        $sql = $this->_disableForeignKeyChecks();
617✔
1663

1664
        if ($sql === '') {
617✔
1665
            // The feature is not supported.
UNCOV
1666
            return false;
×
1667
        }
1668

1669
        return $this->query($sql);
617✔
1670
    }
1671

1672
    /**
1673
     * Enables foreign key checks temporarily.
1674
     *
1675
     * @return bool
1676
     */
1677
    public function enableForeignKeyChecks()
1678
    {
1679
        $sql = $this->_enableForeignKeyChecks();
687✔
1680

1681
        if ($sql === '') {
687✔
1682
            // The feature is not supported.
UNCOV
1683
            return false;
×
1684
        }
1685

1686
        return $this->query($sql);
687✔
1687
    }
1688

1689
    /**
1690
     * Allows the engine to be set into a mode where queries are not
1691
     * actually executed, but they are still generated, timed, etc.
1692
     *
1693
     * This is primarily used by the prepared query functionality.
1694
     *
1695
     * @return $this
1696
     */
1697
    public function pretend(bool $pretend = true)
1698
    {
1699
        $this->pretend = $pretend;
13✔
1700

1701
        return $this;
13✔
1702
    }
1703

1704
    /**
1705
     * Empties our data cache. Especially helpful during testing.
1706
     *
1707
     * @return $this
1708
     */
1709
    public function resetDataCache()
1710
    {
1711
        $this->dataCache = [];
33✔
1712

1713
        return $this;
33✔
1714
    }
1715

1716
    /**
1717
     * Determines if the statement is a write-type query or not.
1718
     *
1719
     * @param string $sql
1720
     */
1721
    public function isWriteType($sql): bool
1722
    {
1723
        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);
713✔
1724
    }
1725

1726
    /**
1727
     * Returns the last error code and message.
1728
     *
1729
     * Must return an array with keys 'code' and 'message':
1730
     *
1731
     * @return         array<string, int|string|null>
1732
     * @phpstan-return array{code: int|string|null, message: string|null}
1733
     */
1734
    abstract public function error(): array;
1735

1736
    /**
1737
     * Insert ID
1738
     *
1739
     * @return int|string
1740
     */
1741
    abstract public function insertID();
1742

1743
    /**
1744
     * Generates the SQL for listing tables in a platform-dependent manner.
1745
     *
1746
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
1747
     *
1748
     * @return false|string
1749
     */
1750
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
1751

1752
    /**
1753
     * Generates a platform-specific query string so that the column names can be fetched.
1754
     *
1755
     * @return false|string
1756
     */
1757
    abstract protected function _listColumns(string $table = '');
1758

1759
    /**
1760
     * Platform-specific field data information.
1761
     *
1762
     * @see    getFieldData()
1763
     */
1764
    abstract protected function _fieldData(string $table): array;
1765

1766
    /**
1767
     * Platform-specific index data.
1768
     *
1769
     * @see    getIndexData()
1770
     *
1771
     * @return array<string, stdClass>
1772
     */
1773
    abstract protected function _indexData(string $table): array;
1774

1775
    /**
1776
     * Platform-specific foreign keys data.
1777
     *
1778
     * @see    getForeignKeyData()
1779
     *
1780
     * @return array<string, stdClass>
1781
     */
1782
    abstract protected function _foreignKeyData(string $table): array;
1783

1784
    /**
1785
     * Platform-specific SQL statement to disable foreign key checks.
1786
     *
1787
     * If this feature is not supported, return empty string.
1788
     *
1789
     * @TODO This method should be moved to an interface that represents foreign key support.
1790
     *
1791
     * @return string
1792
     *
1793
     * @see disableForeignKeyChecks()
1794
     */
1795
    protected function _disableForeignKeyChecks()
1796
    {
UNCOV
1797
        return '';
×
1798
    }
1799

1800
    /**
1801
     * Platform-specific SQL statement to enable foreign key checks.
1802
     *
1803
     * If this feature is not supported, return empty string.
1804
     *
1805
     * @TODO This method should be moved to an interface that represents foreign key support.
1806
     *
1807
     * @return string
1808
     *
1809
     * @see enableForeignKeyChecks()
1810
     */
1811
    protected function _enableForeignKeyChecks()
1812
    {
UNCOV
1813
        return '';
×
1814
    }
1815

1816
    /**
1817
     * Accessor for properties if they exist.
1818
     *
1819
     * @return array|bool|float|int|object|resource|string|null
1820
     */
1821
    public function __get(string $key)
1822
    {
1823
        if (property_exists($this, $key)) {
880✔
1824
            return $this->{$key};
879✔
1825
        }
1826

1827
        return null;
1✔
1828
    }
1829

1830
    /**
1831
     * Checker for properties existence.
1832
     */
1833
    public function __isset(string $key): bool
1834
    {
1835
        return property_exists($this, $key);
206✔
1836
    }
1837
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc