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

codeigniter4 / CodeIgniter4 / 10190052285

01 Aug 2024 01:00AM UTC coverage: 84.498% (+0.04%) from 84.463%
10190052285

push

github

web-flow
Merge pull request #8748 from kenjis/feat-db-tablename-object-4.6

feat: fix spark db:table causes errors with table name including special chars

83 of 86 new or added lines in 9 files covered. (96.51%)

1 existing line in 1 file now uncovered.

20555 of 24326 relevant lines covered (84.5%)

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

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

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

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

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

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

419
        try {
420
            // Connect to the database and set the connection ID
421
            $this->connID = $this->connect($this->pConnect);
41✔
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) {
41✔
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;
39✔
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;
660✔
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;
854✔
570

571
        return $this;
854✔
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 ?: $this->queryClass;
707✔
618

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

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

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

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

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

636
        // Always save the last query so we can use
637
        // the getLastQuery() method.
638
        $this->lastQuery = $query;
707✔
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) {
707✔
644
            $query->setDuration($startTime);
8✔
645

646
            return $query;
8✔
647
        }
648

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

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

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

665
            if (
666
                $this->DBDebug
44✔
667
                && (
668
                    // Not in transactions
669
                    $this->transDepth === 0
44✔
670
                    // In transactions, do not throw exception by default.
44✔
671
                    || $this->transException
44✔
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) {
23✔
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);
23✔
690

691
                if ($exception !== null) {
23✔
692
                    throw new DatabaseException(
6✔
693
                        $exception->getMessage(),
6✔
694
                        $exception->getCode(),
6✔
695
                        $exception
6✔
696
                    );
6✔
697
                }
698

699
                return false;
17✔
700
            }
701

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

705
            return false;
21✔
706
        }
707

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

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

713
        // resultID is not false, so it must be successful
714
        if ($this->isWriteType($sql)) {
707✔
715
            return true;
674✔
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);
707✔
720

721
        return new $resultClass($this->connID, $this->resultID);
707✔
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)) {
714✔
735
            $this->initialize();
6✔
736
        }
737

738
        return $this->execute($sql);
714✔
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;
3✔
770

771
        return $this;
3✔
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 $transExcetion)
792
    {
793
        $this->transException = $transExcetion;
3✔
794

795
        return $this;
3✔
796
    }
797

798
    /**
799
     * Complete Transaction
800
     */
801
    public function transComplete(): bool
802
    {
803
        if (! $this->transEnabled) {
44✔
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) {
44✔
809
            $this->transRollback();
14✔
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) {
14✔
815
                $this->transStatus = true;
3✔
816
            }
817

818
            return false;
14✔
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;
13✔
830
    }
831

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

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

845
            return true;
×
846
        }
847

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

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

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

860
            return true;
46✔
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) {
16✔
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()) {
16✔
896
            $this->transDepth--;
16✔
897

898
            return true;
16✔
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)) {
797✔
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);
797✔
945

946
        return new $className($tableName, $this);
797✔
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
     * @return BasePreparedQuery|null
977
     */
978
    public function prepare(Closure $func, array $options = [])
979
    {
980
        if (empty($this->connID)) {
12✔
981
            $this->initialize();
×
982
        }
983

984
        $this->pretend();
12✔
985

986
        $sql = $func($this);
12✔
987

988
        $this->pretend(false);
12✔
989

990
        if ($sql instanceof QueryInterface) {
12✔
991
            $sql = $sql->getOriginalQuery();
12✔
992
        }
993

994
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
12✔
995
        /** @var BasePreparedQuery $class */
996
        $class = new $class($this);
12✔
997

998
        return $class->prepare($sql, $options);
12✔
999
    }
1000

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

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

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

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

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

1075
        if (is_array($item)) {
959✔
1076
            $escapedArray = [];
1✔
1077

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

1082
            return $escapedArray;
1✔
1083
        }
1084

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

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

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

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

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

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

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

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

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

1151
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
939✔
1152
            $item = $this->escapeIdentifiers($item);
937✔
1153
        }
1154

1155
        return $item . $alias;
939✔
1156
    }
1157

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

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

1176
                $item = implode('.', $parts);
10✔
1177
            }
1178

1179
            return $item . $alias;
10✔
1180
        }
1181

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

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

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

1216
            // Put the parts back together
1217
            $item = implode('.', $parts);
119✔
1218
        }
1219

1220
        if ($protectIdentifiers === true) {
127✔
1221
            $item = $this->escapeIdentifiers($item);
127✔
1222
        }
1223

1224
        return $item . $alias;
127✔
1225
    }
1226

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

1240
        if ($item instanceof TableName) {
609✔
1241
            return $this->escapeTableName($item);
7✔
1242
        }
1243

1244
        return $this->escapeChar
609✔
1245
            . str_replace(
609✔
1246
                $this->escapeChar,
609✔
1247
                $this->escapeChar . $this->escapeChar,
609✔
1248
                $item
609✔
1249
            )
609✔
1250
            . $this->escapeChar;
609✔
1251
    }
1252

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

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

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

1280
        if (is_array($item)) {
954✔
1281
            foreach ($item as $key => $value) {
622✔
1282
                $item[$key] = $this->escapeIdentifiers($value);
622✔
1283
            }
1284

1285
            return $item;
622✔
1286
        }
1287

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

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

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

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

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

1340
        return $this->DBPrefix . $table;
3✔
1341
    }
1342

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

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

1365
        if ($str instanceof Stringable) {
807✔
1366
            if ($str instanceof RawSql) {
13✔
1367
                return $str->__toString();
12✔
1368
            }
1369

1370
            $str = (string) $str;
1✔
1371
        }
1372

1373
        if (is_string($str)) {
804✔
1374
            return "'" . $this->escapeString($str) . "'";
754✔
1375
        }
1376

1377
        if (is_bool($str)) {
725✔
1378
            return ($str === false) ? 0 : 1;
6✔
1379
        }
1380

1381
        return $str ?? 'NULL';
723✔
1382
    }
1383

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

1399
            return $str;
×
1400
        }
1401

1402
        if ($str instanceof Stringable) {
754✔
1403
            if ($str instanceof RawSql) {
2✔
1404
                return $str->__toString();
×
1405
            }
1406

1407
            $str = (string) $str;
2✔
1408
        }
1409

1410
        $str = $this->_escapeString($str);
754✔
1411

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

1429
        return $str;
754✔
1430
    }
1431

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

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

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

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

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

1478
            return false;
×
1479
        }
1480

1481
        return $functionName(...$params);
×
1482
    }
1483

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

1492
    // --------------------------------------------------------------------
1493
    // META Methods
1494
    // --------------------------------------------------------------------
1495

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

1511
        $sql = $this->_listTables($constrainByPrefix);
51✔
1512

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

1518
            return false;
×
1519
        }
1520

1521
        $this->dataCache['table_names'] = [];
51✔
1522

1523
        $query = $this->query($sql);
51✔
1524

1525
        foreach ($query->getResultArray() as $row) {
51✔
1526
            /** @var string $table */
1527
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
48✔
1528

1529
            $this->dataCache['table_names'][] = $table;
48✔
1530
        }
1531

1532
        return $this->dataCache['table_names'];
51✔
1533
    }
1534

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

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

1551
            return false;
×
1552
        }
1553

1554
        $tableExists = $this->query($sql)->getResultArray() !== [];
617✔
1555

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

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

1571
        return $tableExists;
617✔
1572
    }
1573

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

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

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

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

1601
            return false;
×
1602
        }
1603

1604
        $query = $this->query($sql);
8✔
1605

1606
        $this->dataCache['field_names'][$table] = [];
8✔
1607

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

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

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

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

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

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

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

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

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

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

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

1706
            $retVal[$name] = $obj;
12✔
1707
        }
1708

1709
        return $retVal;
34✔
1710
    }
1711

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

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

1726
        return $this->query($sql);
630✔
1727
    }
1728

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

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

1743
        return $this->query($sql);
705✔
1744
    }
1745

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

1758
        return $this;
13✔
1759
    }
1760

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

1770
        return $this;
33✔
1771
    }
1772

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

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

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

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

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

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

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

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

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

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

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

1888
        return null;
1✔
1889
    }
1890

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