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

codeigniter4 / CodeIgniter4 / 28995226889

09 Jul 2026 04:59AM UTC coverage: 89.607% (+0.02%) from 89.588%
28995226889

Pull #10395

github

web-flow
Merge d47c78aa8 into 0d4f36d76
Pull Request #10395: feat: add StreamResponse and SSE response factories

31 of 41 new or added lines in 3 files covered. (75.61%)

223 existing lines in 6 files now uncovered.

25330 of 28268 relevant lines covered (89.61%)

230.92 hits per line

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

91.6
/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 BackedEnum;
17
use Closure;
18
use CodeIgniter\Database\Exceptions\CheckConstraintViolationException;
19
use CodeIgniter\Database\Exceptions\ConstraintViolationException;
20
use CodeIgniter\Database\Exceptions\DatabaseException;
21
use CodeIgniter\Database\Exceptions\ForeignKeyConstraintViolationException;
22
use CodeIgniter\Database\Exceptions\NotNullConstraintViolationException;
23
use CodeIgniter\Database\Exceptions\RetryableTransactionException;
24
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
25
use CodeIgniter\Events\Events;
26
use CodeIgniter\Exceptions\InvalidArgumentException;
27
use CodeIgniter\I18n\Time;
28
use Exception;
29
use ReflectionClass;
30
use ReflectionNamedType;
31
use ReflectionType;
32
use ReflectionUnionType;
33
use stdClass;
34
use Stringable;
35
use Throwable;
36

37
/**
38
 * @property-read array      $aliasedTables
39
 * @property-read string     $charset
40
 * @property-read bool       $compress
41
 * @property-read float      $connectDuration
42
 * @property-read float      $connectTime
43
 * @property-read string     $database
44
 * @property-read array      $dateFormat
45
 * @property-read string     $DBCollat
46
 * @property-read bool       $DBDebug
47
 * @property-read string     $DBDriver
48
 * @property-read string     $DBPrefix
49
 * @property-read string     $DSN
50
 * @property-read array|bool $encrypt
51
 * @property-read array      $failover
52
 * @property-read string     $hostname
53
 * @property-read Query      $lastQuery
54
 * @property-read string     $password
55
 * @property-read bool       $pConnect
56
 * @property-read int|string $port
57
 * @property-read bool       $pretend
58
 * @property-read string     $queryClass
59
 * @property-read array      $reservedIdentifiers
60
 * @property-read string     $subdriver
61
 * @property-read string     $swapPre
62
 * @property-read int        $transDepth
63
 * @property-read bool       $transFailure
64
 * @property-read bool       $transStatus
65
 * @property-read string     $username
66
 *
67
 * @template TConnection
68
 * @template TResult
69
 *
70
 * @implements ConnectionInterface<TConnection, TResult>
71
 * @see \CodeIgniter\Database\BaseConnectionTest
72
 */
73
abstract class BaseConnection implements ConnectionInterface
74
{
75
    /**
76
     * Cached builtin type names per class/property.
77
     *
78
     * @var array<class-string, array<string, list<string>>>
79
     */
80
    private static array $propertyBuiltinTypesCache = [];
81

82
    /**
83
     * Data Source Name / Connect string
84
     *
85
     * @var string
86
     */
87
    protected $DSN;
88

89
    /**
90
     * Database port
91
     *
92
     * @var int|string
93
     */
94
    protected $port = '';
95

96
    /**
97
     * Hostname
98
     *
99
     * @var string
100
     */
101
    protected $hostname;
102

103
    /**
104
     * Username
105
     *
106
     * @var string
107
     */
108
    protected $username;
109

110
    /**
111
     * Password
112
     *
113
     * @var string
114
     */
115
    protected $password;
116

117
    /**
118
     * Database name
119
     *
120
     * @var string
121
     */
122
    protected $database;
123

124
    /**
125
     * Database driver
126
     *
127
     * @var string
128
     */
129
    protected $DBDriver = 'MySQLi';
130

131
    /**
132
     * Sub-driver
133
     *
134
     * @used-by CI_DB_pdo_driver
135
     *
136
     * @var string
137
     */
138
    protected $subdriver;
139

140
    /**
141
     * Table prefix
142
     *
143
     * @var string
144
     */
145
    protected $DBPrefix = '';
146

147
    /**
148
     * Persistent connection flag
149
     *
150
     * @var bool
151
     */
152
    protected $pConnect = false;
153

154
    /**
155
     * Whether to throw Exception or not when an error occurs.
156
     *
157
     * @var bool
158
     */
159
    protected $DBDebug = true;
160

161
    /**
162
     * Character set
163
     *
164
     * This value must be updated by Config\Database if the driver use it.
165
     *
166
     * @var string
167
     */
168
    protected $charset = '';
169

170
    /**
171
     * Collation
172
     *
173
     * This value must be updated by Config\Database if the driver use it.
174
     *
175
     * @var string
176
     */
177
    protected $DBCollat = '';
178

179
    /**
180
     * Database session timezone
181
     *
182
     * false    = Don't set timezone (default, backward compatible)
183
     * true     = Automatically sync with app timezone
184
     * string   = Specific timezone (offset or named timezone)
185
     *
186
     * Named timezones (e.g., 'America/New_York') will be automatically
187
     * converted to offsets (e.g., '-05:00') for database compatibility.
188
     *
189
     * @var bool|string
190
     */
191
    protected $timezone = false;
192

193
    /**
194
     * Swap Prefix
195
     *
196
     * @var string
197
     */
198
    protected $swapPre = '';
199

200
    /**
201
     * Encryption flag/data
202
     *
203
     * @var array|bool
204
     */
205
    protected $encrypt = false;
206

207
    /**
208
     * Compression flag
209
     *
210
     * @var bool
211
     */
212
    protected $compress = false;
213

214
    /**
215
     * Settings for a failover connection.
216
     *
217
     * @var array
218
     */
219
    protected $failover = [];
220

221
    /**
222
     * The last query object that was executed
223
     * on this connection.
224
     *
225
     * @var Query
226
     */
227
    protected $lastQuery;
228

229
    /**
230
     * The exception that would have been thrown on the last failed query
231
     * if DBDebug were enabled. Null when the last query succeeded or when
232
     * DBDebug is true (in which case the exception is thrown directly and
233
     * this property is never set).
234
     */
235
    protected ?DatabaseException $lastException = null;
236

237
    /**
238
     * The first database exception that caused the current transaction to fail.
239
     */
240
    protected ?DatabaseException $transFailureException = null;
241

242
    /**
243
     * Connection ID
244
     *
245
     * @var false|TConnection
246
     */
247
    public $connID = false;
248

249
    /**
250
     * Result ID
251
     *
252
     * @var false|TResult
253
     */
254
    public $resultID = false;
255

256
    /**
257
     * Protect identifiers flag
258
     *
259
     * @var bool
260
     */
261
    public $protectIdentifiers = true;
262

263
    /**
264
     * List of reserved identifiers
265
     *
266
     * Identifiers that must NOT be escaped.
267
     *
268
     * @var array
269
     */
270
    protected $reservedIdentifiers = ['*'];
271

272
    /**
273
     * Identifier escape character
274
     *
275
     * @var array|string
276
     */
277
    public $escapeChar = '"';
278

279
    /**
280
     * ESCAPE statement string
281
     *
282
     * @var string
283
     */
284
    public $likeEscapeStr = " ESCAPE '%s' ";
285

286
    /**
287
     * ESCAPE character
288
     *
289
     * @var string
290
     */
291
    public $likeEscapeChar = '!';
292

293
    /**
294
     * RegExp used to escape identifiers
295
     *
296
     * @var array
297
     */
298
    protected $pregEscapeChar = [];
299

300
    /**
301
     * Holds previously looked up data
302
     * for performance reasons.
303
     *
304
     * @var array
305
     */
306
    public $dataCache = [];
307

308
    /**
309
     * Microtime when connection was made
310
     *
311
     * @var float
312
     */
313
    protected $connectTime = 0.0;
314

315
    /**
316
     * How long it took to establish connection.
317
     *
318
     * @var float
319
     */
320
    protected $connectDuration = 0.0;
321

322
    /**
323
     * If true, no queries will actually be
324
     * run against the database.
325
     *
326
     * @var bool
327
     */
328
    protected $pretend = false;
329

330
    /**
331
     * Transaction enabled flag
332
     *
333
     * @var bool
334
     */
335
    public $transEnabled = true;
336

337
    /**
338
     * Strict transaction mode flag
339
     *
340
     * @var bool
341
     */
342
    public $transStrict = true;
343

344
    /**
345
     * Transaction depth level
346
     *
347
     * @var int
348
     */
349
    protected $transDepth = 0;
350

351
    /**
352
     * Transaction status flag
353
     *
354
     * Used with transactions to determine if a rollback should occur.
355
     *
356
     * @var bool
357
     */
358
    protected $transStatus = true;
359

360
    /**
361
     * Transaction failure flag
362
     *
363
     * Used with transactions to determine if a transaction has failed.
364
     *
365
     * @var bool
366
     */
367
    protected $transFailure = false;
368

369
    /**
370
     * Whether to throw exceptions during transaction
371
     */
372
    protected bool $transException = false;
373

374
    /**
375
     * Callbacks to run after the outermost transaction commits.
376
     *
377
     * @var list<callable(): void>
378
     */
379
    protected array $transCommitCallbacks = [];
380

381
    /**
382
     * Callbacks to run after the outermost transaction rolls back.
383
     *
384
     * @var list<callable(): void>
385
     */
386
    protected array $transRollbackCallbacks = [];
387

388
    /**
389
     * Array of table aliases.
390
     *
391
     * @var list<string>
392
     */
393
    protected $aliasedTables = [];
394

395
    /**
396
     * Query Class
397
     *
398
     * @var string
399
     */
400
    protected $queryClass = Query::class;
401

402
    /**
403
     * Default Date/Time formats
404
     *
405
     * @var array<string, string>
406
     */
407
    protected array $dateFormat = [
408
        'date'        => 'Y-m-d',
409
        'datetime'    => 'Y-m-d H:i:s',
410
        'datetime-ms' => 'Y-m-d H:i:s.v',
411
        'datetime-us' => 'Y-m-d H:i:s.u',
412
        'time'        => 'H:i:s',
413
    ];
414

415
    /**
416
     * Saves our connection settings.
417
     */
418
    public function __construct(array $params)
419
    {
420
        if (isset($params['dateFormat'])) {
698✔
421
            $this->dateFormat = array_merge($this->dateFormat, $params['dateFormat']);
180✔
422
            unset($params['dateFormat']);
180✔
423
        }
424

425
        $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($params));
698✔
426

427
        foreach ($params as $key => $value) {
698✔
428
            if (property_exists($this, $key)) {
258✔
429
                $this->{$key} = $this->castScalarValueForTypedProperty(
258✔
430
                    $value,
258✔
431
                    $typedPropertyTypes[$key] ?? [],
258✔
432
                );
258✔
433
            }
434
        }
435

436
        $queryClass = str_replace('Connection', 'Query', static::class);
697✔
437

438
        if (class_exists($queryClass)) {
697✔
439
            $this->queryClass = $queryClass;
553✔
440
        }
441

442
        if ($this->failover !== []) {
697✔
443
            // If there is a failover database, connect now to do failover.
444
            // Otherwise, Query Builder creates SQL statement with the main database config
445
            // (DBPrefix) even when the main database is down.
446
            $this->initialize();
2✔
447
        }
448
    }
449

450
    /**
451
     * Some config values (especially env overrides without clear source type)
452
     * can still reach us as strings. Coerce them for typed properties to keep
453
     * strict typing compatible.
454
     *
455
     * @param list<string> $types
456
     */
457
    private function castScalarValueForTypedProperty(mixed $value, array $types): mixed
458
    {
459
        if (! is_string($value)) {
258✔
460
            return $value;
202✔
461
        }
462

463
        if ($types === [] || in_array('string', $types, true) || in_array('mixed', $types, true)) {
258✔
464
            return $value;
258✔
465
        }
466

467
        $trimmedValue = trim($value);
5✔
468

469
        if (in_array('null', $types, true) && strtolower($trimmedValue) === 'null') {
5✔
470
            return null;
1✔
471
        }
472

473
        if (in_array('int', $types, true) && preg_match('/^[+-]?\d+$/', $trimmedValue) === 1) {
5✔
474
            return (int) $trimmedValue;
2✔
475
        }
476

477
        if (in_array('float', $types, true) && is_numeric($trimmedValue)) {
4✔
UNCOV
478
            return (float) $trimmedValue;
×
479
        }
480

481
        if (in_array('bool', $types, true) || in_array('false', $types, true) || in_array('true', $types, true)) {
4✔
482
            $boolValue = filter_var($trimmedValue, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
3✔
483

484
            if ($boolValue !== null) {
3✔
485
                if (in_array('bool', $types, true)) {
3✔
486
                    return $boolValue;
2✔
487
                }
488

489
                if ($boolValue === false && in_array('false', $types, true)) {
1✔
490
                    return false;
1✔
491
                }
492

493
                if ($boolValue === true && in_array('true', $types, true)) {
1✔
494
                    return true;
1✔
495
                }
496
            }
497
        }
498

499
        return $value;
1✔
500
    }
501

502
    /**
503
     * @param list<string> $properties
504
     *
505
     * @return array<string, list<string>>
506
     */
507
    private function getBuiltinPropertyTypesMap(array $properties): array
508
    {
509
        $className = static::class;
698✔
510
        $requested = array_fill_keys($properties, true);
698✔
511

512
        if (! isset(self::$propertyBuiltinTypesCache[$className])) {
698✔
513
            self::$propertyBuiltinTypesCache[$className] = [];
26✔
514
        }
515

516
        // Fill only the properties requested by this call that are not cached yet.
517
        $missing = array_diff_key($requested, self::$propertyBuiltinTypesCache[$className]);
698✔
518

519
        if ($missing !== []) {
698✔
520
            $reflection = new ReflectionClass($className);
38✔
521

522
            foreach ($reflection->getProperties() as $property) {
38✔
523
                $propertyName = $property->getName();
38✔
524

525
                if (! isset($missing[$propertyName])) {
38✔
526
                    continue;
38✔
527
                }
528

529
                $type = $property->getType();
38✔
530

531
                if (! $type instanceof ReflectionType) {
38✔
532
                    self::$propertyBuiltinTypesCache[$className][$propertyName] = [];
37✔
533

534
                    continue;
37✔
535
                }
536

537
                $namedTypes   = $type instanceof ReflectionUnionType ? $type->getTypes() : [$type];
18✔
538
                $builtinTypes = [];
18✔
539

540
                foreach ($namedTypes as $namedType) {
18✔
541
                    if (! $namedType instanceof ReflectionNamedType || ! $namedType->isBuiltin()) {
18✔
UNCOV
542
                        continue;
×
543
                    }
544

545
                    $builtinTypes[] = $namedType->getName();
18✔
546
                }
547

548
                if ($type->allowsNull() && ! in_array('null', $builtinTypes, true)) {
18✔
549
                    $builtinTypes[] = 'null';
13✔
550
                }
551

552
                self::$propertyBuiltinTypesCache[$className][$propertyName] = $builtinTypes;
18✔
553
            }
554

555
            // Untyped or unresolved properties are cached as empty to avoid re-reflecting them.
556
            foreach (array_keys($missing) as $propertyName) {
38✔
557
                self::$propertyBuiltinTypesCache[$className][$propertyName] ??= [];
38✔
558
            }
559
        }
560

561
        $typedProperties = [];
698✔
562

563
        foreach ($properties as $property) {
698✔
564
            $typedProperties[$property] = self::$propertyBuiltinTypesCache[$className][$property] ?? [];
258✔
565
        }
566

567
        return $typedProperties;
698✔
568
    }
569

570
    /**
571
     * Initializes the database connection/settings.
572
     *
573
     * @return void
574
     *
575
     * @throws DatabaseException
576
     */
577
    public function initialize()
578
    {
579
        /* If an established connection is available, then there's
580
         * no need to connect and select the database.
581
         *
582
         * Depending on the database driver, connID can be either
583
         * boolean TRUE, a resource or an object.
584
         */
585
        if ($this->connID) {
1,005✔
586
            return;
907✔
587
        }
588

589
        $this->connectTime = microtime(true);
114✔
590
        $connectionErrors  = [];
114✔
591

592
        try {
593
            // Connect to the database and set the connection ID
594
            $this->connID = $this->connect($this->pConnect);
114✔
595
        } catch (Throwable $e) {
2✔
596
            $this->connID       = false;
2✔
597
            $connectionErrors[] = sprintf(
2✔
598
                'Main connection [%s]: %s',
2✔
599
                $this->DBDriver,
2✔
600
                $e->getMessage(),
2✔
601
            );
2✔
602
            log_message('error', 'Error connecting to the database: ' . $e);
2✔
603
        }
604

605
        // No connection resource? Check if there is a failover else throw an error
606
        if (! $this->connID) {
114✔
607
            // Check if there is a failover set
608
            if (! empty($this->failover) && is_array($this->failover)) {
4✔
609
                // Go over all the failovers
610
                foreach ($this->failover as $index => $failover) {
2✔
611
                    $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($failover));
2✔
612

613
                    // Replace the current settings with those of the failover
614
                    foreach ($failover as $key => $val) {
2✔
615
                        if (property_exists($this, $key)) {
2✔
616
                            $this->{$key} = $this->castScalarValueForTypedProperty(
2✔
617
                                $val,
2✔
618
                                $typedPropertyTypes[$key] ?? [],
2✔
619
                            );
2✔
620
                        }
621
                    }
622

623
                    try {
624
                        // Try to connect
625
                        $this->connID = $this->connect($this->pConnect);
2✔
626
                    } catch (Throwable $e) {
1✔
627
                        $connectionErrors[] = sprintf(
1✔
628
                            'Failover #%d [%s]: %s',
1✔
629
                            ++$index,
1✔
630
                            $this->DBDriver,
1✔
631
                            $e->getMessage(),
1✔
632
                        );
1✔
633
                        log_message('error', 'Error connecting to the database: ' . $e);
1✔
634
                    }
635

636
                    // If a connection is made break the foreach loop
637
                    if ($this->connID) {
2✔
638
                        break;
2✔
639
                    }
640
                }
641
            }
642

643
            // We still don't have a connection?
644
            if (! $this->connID) {
4✔
645
                throw new DatabaseException(sprintf(
2✔
646
                    'Unable to connect to the database.%s%s',
2✔
647
                    PHP_EOL,
2✔
648
                    implode(PHP_EOL, $connectionErrors),
2✔
649
                ));
2✔
650
            }
651
        }
652

653
        $this->connectDuration = microtime(true) - $this->connectTime;
112✔
654
    }
655

656
    /**
657
     * Close the database connection.
658
     *
659
     * @return void
660
     */
661
    public function close()
662
    {
663
        if ($this->connID) {
10✔
664
            $this->_close();
7✔
665
            $this->connID = false;
7✔
666
        }
667
    }
668

669
    /**
670
     * Keep or establish the connection if no queries have been sent for
671
     * a length of time exceeding the server's idle timeout.
672
     *
673
     * @return void
674
     */
675
    public function reconnect()
676
    {
677
        if ($this->ping() === false) {
2✔
678
            $this->close();
1✔
679
            $this->initialize();
1✔
680
        }
681
    }
682

683
    /**
684
     * Platform dependent way method for closing the connection.
685
     *
686
     * @return void
687
     */
688
    abstract protected function _close();
689

690
    /**
691
     * Check if the connection is still alive.
692
     */
693
    public function ping(): bool
694
    {
695
        if ($this->connID === false) {
5✔
696
            return false;
2✔
697
        }
698

699
        return $this->_ping();
4✔
700
    }
701

702
    /**
703
     * Driver-specific ping implementation.
704
     */
705
    protected function _ping(): bool
706
    {
707
        try {
708
            $result = $this->simpleQuery('SELECT 1');
4✔
709

710
            return $result !== false;
4✔
UNCOV
711
        } catch (DatabaseException) {
×
UNCOV
712
            return false;
×
713
        }
714
    }
715

716
    /**
717
     * Create a persistent database connection.
718
     *
719
     * @return false|TConnection
720
     */
721
    public function persistentConnect()
722
    {
UNCOV
723
        return $this->connect(true);
×
724
    }
725

726
    /**
727
     * Returns the actual connection object. If both a 'read' and 'write'
728
     * connection has been specified, you can pass either term in to
729
     * get that connection. If you pass either alias in and only a single
730
     * connection is present, it must return the sole connection.
731
     *
732
     * @return false|TConnection
733
     */
734
    public function getConnection(?string $alias = null)
735
    {
736
        // @todo work with read/write connections
737
        return $this->connID;
2✔
738
    }
739

740
    /**
741
     * Returns the name of the current database being used.
742
     */
743
    public function getDatabase(): string
744
    {
745
        return empty($this->database) ? '' : $this->database;
943✔
746
    }
747

748
    /**
749
     * Set DB Prefix
750
     *
751
     * Set's the DB Prefix to something new without needing to reconnect
752
     *
753
     * @param string $prefix The prefix
754
     */
755
    public function setPrefix(string $prefix = ''): string
756
    {
757
        return $this->DBPrefix = $prefix;
15✔
758
    }
759

760
    /**
761
     * Returns the database prefix.
762
     */
763
    public function getPrefix(): string
764
    {
765
        return $this->DBPrefix;
17✔
766
    }
767

768
    /**
769
     * The name of the platform in use (MySQLi, Postgre, SQLite3, OCI8, etc)
770
     */
771
    public function getPlatform(): string
772
    {
773
        return $this->DBDriver;
27✔
774
    }
775

776
    /**
777
     * Sets the Table Aliases to use. These are typically
778
     * collected during use of the Builder, and set here
779
     * so queries are built correctly.
780
     *
781
     * @return $this
782
     */
783
    public function setAliasedTables(array $aliases)
784
    {
785
        $this->aliasedTables = $aliases;
1,244✔
786

787
        return $this;
1,244✔
788
    }
789

790
    /**
791
     * Add a table alias to our list.
792
     *
793
     * @return $this
794
     */
795
    public function addTableAlias(string $alias)
796
    {
797
        if ($alias === '') {
50✔
798
            return $this;
7✔
799
        }
800

801
        if (! in_array($alias, $this->aliasedTables, true)) {
43✔
802
            $this->aliasedTables[] = $alias;
43✔
803
        }
804

805
        return $this;
43✔
806
    }
807

808
    /**
809
     * Executes the query against the database.
810
     *
811
     * @return false|TResult
812
     */
813
    abstract protected function execute(string $sql);
814

815
    /**
816
     * Orchestrates a query against the database. Queries must use
817
     * Database\Statement objects to store the query and build it.
818
     * This method works with the cache.
819
     *
820
     * Should automatically handle different connections for read/write
821
     * queries if needed.
822
     *
823
     * @param array<int|string, mixed>|string|null $binds
824
     *
825
     * @return BaseResult<TConnection, TResult>|bool|Query
826
     *
827
     * @todo BC set $queryClass default as null in 4.1
828
     */
829
    public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '')
830
    {
831
        $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass;
980✔
832

833
        if (empty($this->connID)) {
980✔
834
            $this->initialize();
69✔
835
        }
836

837
        /** @var Query $query */
838
        $query = new $queryClass($this);
980✔
839

840
        $query->setQuery($sql, $binds, $setEscapeFlags);
980✔
841

842
        if (! empty($this->swapPre) && ! empty($this->DBPrefix)) {
980✔
UNCOV
843
            $query->swapPrefix($this->DBPrefix, $this->swapPre);
×
844
        }
845

846
        $startTime = microtime(true);
980✔
847

848
        // Always save the last query so we can use
849
        // the getLastQuery() method.
850
        $this->lastQuery = $query;
980✔
851

852
        // If $pretend is true, then we just want to return
853
        // the actual query object here. There won't be
854
        // any results to return.
855
        if ($this->pretend) {
980✔
856
            $query->setDuration($startTime);
12✔
857

858
            return $query;
12✔
859
        }
860

861
        // Run the query for real
862
        try {
863
            $exception           = null;
980✔
864
            $this->lastException = null;
980✔
865
            $this->resultID      = $this->simpleQuery($query->getQuery());
980✔
866
        } catch (DatabaseException $exception) {
26✔
867
            $this->resultID = false;
26✔
868
        }
869

870
        if ($this->resultID === false) {
980✔
871
            $query->setDuration($startTime, $startTime);
54✔
872

873
            // This will trigger a rollback if transactions are being used
874
            $this->handleTransStatus($exception ?? $this->lastException);
54✔
875

876
            if (
877
                $this->DBDebug
54✔
878
                && (
879
                    // Not in transactions
880
                    $this->transDepth === 0
54✔
881
                    // In transactions, do not throw exception by default.
54✔
882
                    || $this->transException
54✔
883
                )
884
            ) {
885
                // We call this function in order to roll-back queries
886
                // if transactions are enabled. If we don't call this here
887
                // the error message will trigger an exit, causing the
888
                // transactions to remain in limbo.
889
                while ($this->transDepth !== 0) {
21✔
890
                    $transDepth = $this->transDepth;
6✔
891
                    $this->transComplete();
6✔
892

893
                    if ($transDepth === $this->transDepth) {
6✔
UNCOV
894
                        log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
×
UNCOV
895
                        break;
×
896
                    }
897
                }
898

899
                // Let others do something with this query.
900
                Events::trigger('DBQuery', $query);
21✔
901

902
                if ($exception instanceof DatabaseException) {
21✔
903
                    throw $exception;
19✔
904
                }
905

906
                return false;
2✔
907
            }
908

909
            // Let others do something with this query.
910
            Events::trigger('DBQuery', $query);
33✔
911

912
            return false;
33✔
913
        }
914

915
        $query->setDuration($startTime);
979✔
916

917
        // Let others do something with this query
918
        Events::trigger('DBQuery', $query);
979✔
919

920
        // resultID is not false, so it must be successful
921
        if ($this->isWriteType($sql)) {
979✔
922
            return true;
945✔
923
        }
924

925
        // query is not write-type, so it must be read-type query; return QueryResult
926
        $resultClass = str_replace('Connection', 'Result', static::class);
978✔
927

928
        return new $resultClass($this->connID, $this->resultID);
978✔
929
    }
930

931
    /**
932
     * Performs a basic query against the database. No binding or caching
933
     * is performed, nor are transactions handled. Simply takes a raw
934
     * query string and returns the database-specific result id.
935
     *
936
     * @return false|TResult
937
     */
938
    public function simpleQuery(string $sql)
939
    {
940
        if (empty($this->connID)) {
992✔
941
            $this->initialize();
11✔
942
        }
943

944
        return $this->execute($sql);
992✔
945
    }
946

947
    /**
948
     * Disable Transactions
949
     *
950
     * This permits transactions to be disabled at run-time.
951
     *
952
     * @return void
953
     */
954
    public function transOff()
955
    {
956
        $this->transEnabled = false;
2✔
957
    }
958

959
    /**
960
     * Enable/disable Transaction Strict Mode
961
     *
962
     * When strict mode is enabled, if you are running multiple groups of
963
     * transactions, if one group fails all subsequent groups will be
964
     * rolled back.
965
     *
966
     * If strict mode is disabled, each group is treated autonomously,
967
     * meaning a failure of one group will not affect any others
968
     *
969
     * @param bool $mode = true
970
     *
971
     * @return $this
972
     */
973
    public function transStrict(bool $mode = true)
974
    {
975
        $this->transStrict = $mode;
6✔
976

977
        return $this;
6✔
978
    }
979

980
    /**
981
     * Start Transaction
982
     */
983
    public function transStart(bool $testMode = false): bool
984
    {
985
        if (! $this->transEnabled) {
66✔
UNCOV
986
            return false;
×
987
        }
988

989
        return $this->transBegin($testMode);
66✔
990
    }
991

992
    /**
993
     * If set to true, exceptions are thrown during transactions.
994
     *
995
     * @return $this
996
     */
997
    public function transException(bool $transException)
998
    {
999
        $this->transException = $transException;
6✔
1000

1001
        return $this;
6✔
1002
    }
1003

1004
    /**
1005
     * Complete Transaction
1006
     */
1007
    public function transComplete(): bool
1008
    {
1009
        if (! $this->transEnabled) {
84✔
UNCOV
1010
            return false;
×
1011
        }
1012

1013
        // The query() function will set this flag to FALSE in the event that a query failed
1014
        if ($this->transStatus === false || $this->transFailure === true) {
84✔
1015
            try {
1016
                $this->transRollback();
35✔
1017
            } finally {
1018
                // If we are NOT running in strict mode, we will reset
1019
                // the _trans_status flag so that subsequent groups of
1020
                // transactions will be permitted.
1021
                if ($this->transStrict === false) {
35✔
1022
                    $this->transStatus = true;
35✔
1023
                }
1024
            }
1025

1026
            return false;
33✔
1027
        }
1028

1029
        return $this->transCommit();
58✔
1030
    }
1031

1032
    /**
1033
     * Lets you retrieve the transaction flag to determine if it has failed
1034
     */
1035
    public function transStatus(): bool
1036
    {
1037
        return $this->transStatus;
21✔
1038
    }
1039

1040
    /**
1041
     * Checks whether this connection is inside an active transaction.
1042
     */
1043
    public function inTransaction(): bool
1044
    {
1045
        return $this->transDepth > 0;
6✔
1046
    }
1047

1048
    /**
1049
     * Register a callback to run after the outermost transaction commits.
1050
     *
1051
     * If no transaction is active, the callback runs immediately.
1052
     *
1053
     * @param callable(): void $callback
1054
     *
1055
     * @return $this
1056
     */
1057
    public function afterCommit(callable $callback): static
1058
    {
1059
        if ($this->transDepth === 0) {
13✔
1060
            $callback();
2✔
1061

1062
            return $this;
2✔
1063
        }
1064

1065
        $this->transCommitCallbacks[] = $callback;
11✔
1066

1067
        return $this;
11✔
1068
    }
1069

1070
    /**
1071
     * Register a callback to run after the outermost transaction rolls back.
1072
     *
1073
     * If no transaction is active, the callback is not run.
1074
     *
1075
     * @param callable(): void $callback
1076
     *
1077
     * @return $this
1078
     */
1079
    public function afterRollback(callable $callback): static
1080
    {
1081
        if ($this->transDepth === 0) {
15✔
1082
            return $this;
1✔
1083
        }
1084

1085
        $this->transRollbackCallbacks[] = $callback;
14✔
1086

1087
        return $this;
14✔
1088
    }
1089

1090
    /**
1091
     * Run the callback inside a transaction.
1092
     *
1093
     * @template TReturn
1094
     *
1095
     * @param callable(self): TReturn $callback
1096
     * @param positive-int            $attempts
1097
     * @param bool|null               $transException   Temporarily override transaction exception mode.
1098
     * @param bool                    $resetTransStatus Reset transaction status before an outermost transaction starts.
1099
     *
1100
     * @return false|TReturn
1101
     */
1102
    public function transaction(
1103
        callable $callback,
1104
        int $attempts = 1,
1105
        ?bool $transException = null,
1106
        bool $resetTransStatus = false,
1107
    ): mixed {
1108
        if ($attempts < 1) {
36✔
1109
            throw new InvalidArgumentException('Transaction attempts must be a positive integer.');
1✔
1110
        }
1111

1112
        $restoreTransException  = $transException !== null;
35✔
1113
        $previousTransException = $this->transException;
35✔
1114

1115
        if ($restoreTransException) {
35✔
1116
            $this->transException = $transException;
5✔
1117
        }
1118

1119
        try {
1120
            if (! $this->transEnabled) {
35✔
1121
                return $callback($this);
1✔
1122
            }
1123

1124
            $outermostTransaction = $this->transDepth === 0;
34✔
1125

1126
            if ($resetTransStatus && $outermostTransaction) {
34✔
1127
                $this->resetTransStatus();
1✔
1128
            }
1129

1130
            $attempts = $outermostTransaction ? $attempts : 1;
34✔
1131

1132
            for ($attempt = 1; $attempt <= $attempts; $attempt++) {
34✔
1133
                if (! $this->transBegin()) {
34✔
1134
                    return false;
1✔
1135
                }
1136

1137
                try {
1138
                    $result = $callback($this);
33✔
1139
                } catch (Throwable $e) {
16✔
1140
                    try {
1141
                        $this->transRollback();
16✔
1142
                    } catch (Throwable $rollbackException) {
2✔
1143
                        log_message('error', 'Database: Transaction callback threw an exception before rollback failed: ' . $e);
2✔
1144

1145
                        throw $rollbackException;
2✔
1146
                    } finally {
1147
                        if ($this->transDepth > 0) {
16✔
1148
                            $this->transStatus = false;
3✔
1149
                        } elseif ($this->transStrict === false) {
13✔
1150
                            $this->transStatus = true;
16✔
1151
                        }
1152
                    }
1153

1154
                    if ($this->transDepth === 0 && $e instanceof RetryableTransactionException && $attempt < $attempts) {
14✔
1155
                        $this->prepareTransactionRetry();
5✔
1156

1157
                        continue;
5✔
1158
                    }
1159

1160
                    throw $e;
10✔
1161
                }
1162

1163
                if (! $this->transComplete()) {
21✔
1164
                    if ($this->transDepth === 0 && $this->transFailureException instanceof RetryableTransactionException && $attempt < $attempts) {
9✔
1165
                        $this->prepareTransactionRetry();
2✔
1166

1167
                        continue;
2✔
1168
                    }
1169

1170
                    return false;
7✔
1171
                }
1172

1173
                return $result;
14✔
1174
            }
1175

UNCOV
1176
            return false;
×
1177
        } finally {
1178
            if ($restoreTransException) {
35✔
1179
                $this->transException = $previousTransException;
35✔
1180
            }
1181
        }
1182
    }
1183

1184
    /**
1185
     * Begin Transaction
1186
     */
1187
    public function transBegin(bool $testMode = false): bool
1188
    {
1189
        if (! $this->transEnabled) {
105✔
1190
            return false;
1✔
1191
        }
1192

1193
        // When transactions are nested we only begin/commit/rollback the outermost ones
1194
        if ($this->transDepth > 0) {
104✔
1195
            $this->transDepth++;
9✔
1196

1197
            return true;
9✔
1198
        }
1199

1200
        if (empty($this->connID)) {
104✔
1201
            $this->initialize();
12✔
1202
        }
1203

1204
        // Reset the transaction failure flag.
1205
        // If the $testMode flag is set to TRUE transactions will be rolled back
1206
        // even if the queries produce a successful result.
1207
        $this->transFailure          = $testMode;
104✔
1208
        $this->transFailureException = null;
104✔
1209

1210
        if ($this->_transBegin()) {
104✔
1211
            $this->transDepth++;
103✔
1212

1213
            return true;
103✔
1214
        }
1215

1216
        return false;
1✔
1217
    }
1218

1219
    /**
1220
     * Commit Transaction
1221
     */
1222
    public function transCommit(): bool
1223
    {
1224
        if (! $this->transEnabled || $this->transDepth === 0) {
62✔
UNCOV
1225
            return false;
×
1226
        }
1227

1228
        // When transactions are nested we only begin/commit/rollback the outermost ones
1229
        if ($this->transDepth > 1 || $this->_transCommit()) {
62✔
1230
            $this->transDepth--;
62✔
1231

1232
            if ($this->transDepth === 0) {
62✔
1233
                $this->transRollbackCallbacks = [];
61✔
1234
                $this->runTransCommitCallbacks();
61✔
1235
            }
1236

1237
            return true;
60✔
1238
        }
1239

1240
        return false;
1✔
1241
    }
1242

1243
    /**
1244
     * Rollback Transaction
1245
     */
1246
    public function transRollback(): bool
1247
    {
1248
        if (! $this->transEnabled || $this->transDepth === 0) {
55✔
1249
            return false;
3✔
1250
        }
1251

1252
        // When transactions are nested we only begin/commit/rollback the outermost ones
1253
        if ($this->transDepth > 1 || $this->_transRollback()) {
55✔
1254
            $this->transDepth--;
53✔
1255

1256
            if ($this->transDepth === 0) {
53✔
1257
                $this->transCommitCallbacks = [];
53✔
1258
                $this->runTransRollbackCallbacks();
53✔
1259
            }
1260

1261
            return true;
48✔
1262
        }
1263

1264
        return false;
3✔
1265
    }
1266

1267
    /**
1268
     * Reset transaction status - to restart transactions after strict mode failure
1269
     */
1270
    public function resetTransStatus(): static
1271
    {
1272
        $this->transStatus = true;
6✔
1273

1274
        return $this;
6✔
1275
    }
1276

1277
    /**
1278
     * Handle transaction status when a query fails
1279
     *
1280
     * @internal This method is for internal database component use only
1281
     */
1282
    public function handleTransStatus(?DatabaseException $exception = null): void
1283
    {
1284
        if ($this->transDepth !== 0) {
66✔
1285
            $this->transStatus = false;
32✔
1286
            $this->transFailureException ??= $exception;
32✔
1287
        }
1288
    }
1289

1290
    /**
1291
     * Reset transaction state that should not leak into a retry attempt.
1292
     */
1293
    protected function prepareTransactionRetry(): void
1294
    {
1295
        $this->transStatus           = true;
7✔
1296
        $this->transFailureException = null;
7✔
1297
        $this->lastException         = null;
7✔
1298
    }
1299

1300
    /**
1301
     * Run and clear callbacks registered for a successful transaction commit.
1302
     */
1303
    protected function runTransCommitCallbacks(): void
1304
    {
1305
        $callbacks                  = $this->transCommitCallbacks;
61✔
1306
        $this->transCommitCallbacks = [];
61✔
1307

1308
        foreach ($callbacks as $callback) {
61✔
1309
            $callback();
10✔
1310
        }
1311
    }
1312

1313
    /**
1314
     * Run and clear callbacks registered for a transaction rollback.
1315
     */
1316
    protected function runTransRollbackCallbacks(): void
1317
    {
1318
        $callbacks                    = $this->transRollbackCallbacks;
53✔
1319
        $this->transRollbackCallbacks = [];
53✔
1320

1321
        foreach ($callbacks as $callback) {
53✔
1322
            $callback();
13✔
1323
        }
1324
    }
1325

1326
    /**
1327
     * Begin Transaction
1328
     */
1329
    abstract protected function _transBegin(): bool;
1330

1331
    /**
1332
     * Commit Transaction
1333
     */
1334
    abstract protected function _transCommit(): bool;
1335

1336
    /**
1337
     * Rollback Transaction
1338
     */
1339
    abstract protected function _transRollback(): bool;
1340

1341
    /**
1342
     * Returns a non-shared new instance of the query builder for this connection.
1343
     *
1344
     * @param array|string|TableName $tableName
1345
     *
1346
     * @return BaseBuilder
1347
     *
1348
     * @throws DatabaseException
1349
     */
1350
    public function table($tableName)
1351
    {
1352
        if (empty($tableName)) {
1,143✔
UNCOV
1353
            throw new DatabaseException('You must set the database table to be used with your query.');
×
1354
        }
1355

1356
        $className = str_replace('Connection', 'Builder', static::class);
1,143✔
1357

1358
        return new $className($tableName, $this);
1,143✔
1359
    }
1360

1361
    /**
1362
     * Returns a new instance of the BaseBuilder class with a cleared FROM clause.
1363
     */
1364
    public function newQuery(): BaseBuilder
1365
    {
1366
        // save table aliases
1367
        $tempAliases         = $this->aliasedTables;
22✔
1368
        $builder             = $this->table(',')->from([], true);
22✔
1369
        $this->aliasedTables = $tempAliases;
22✔
1370

1371
        return $builder;
22✔
1372
    }
1373

1374
    /**
1375
     * Creates a prepared statement with the database that can then
1376
     * be used to execute multiple statements against. Within the
1377
     * closure, you would build the query in any normal way, though
1378
     * the Query Builder is the expected manner.
1379
     *
1380
     * Example:
1381
     *    $stmt = $db->prepare(function($db)
1382
     *           {
1383
     *             return $db->table('users')
1384
     *                   ->where('id', 1)
1385
     *                     ->get();
1386
     *           })
1387
     *
1388
     * @param Closure(BaseConnection): mixed $func
1389
     *
1390
     * @return BasePreparedQuery|null
1391
     */
1392
    public function prepare(Closure $func, array $options = [])
1393
    {
1394
        if (empty($this->connID)) {
18✔
UNCOV
1395
            $this->initialize();
×
1396
        }
1397

1398
        $this->pretend();
18✔
1399

1400
        $sql = $func($this);
18✔
1401

1402
        $this->pretend(false);
18✔
1403

1404
        if ($sql instanceof QueryInterface) {
18✔
1405
            $sql = $sql->getOriginalQuery();
18✔
1406
        }
1407

1408
        $class = str_ireplace('Connection', 'PreparedQuery', static::class);
18✔
1409
        /** @var BasePreparedQuery $class */
1410
        $class = new $class($this);
18✔
1411

1412
        return $class->prepare($sql, $options);
18✔
1413
    }
1414

1415
    /**
1416
     * Returns the last query's statement object.
1417
     *
1418
     * @return Query
1419
     */
1420
    public function getLastQuery()
1421
    {
1422
        return $this->lastQuery;
15✔
1423
    }
1424

1425
    /**
1426
     * Returns a string representation of the last query's statement object.
1427
     */
1428
    public function showLastQuery(): string
1429
    {
UNCOV
1430
        return (string) $this->lastQuery;
×
1431
    }
1432

1433
    /**
1434
     * Returns the time we started to connect to this database in
1435
     * seconds with microseconds.
1436
     *
1437
     * Used by the Debug Toolbar's timeline.
1438
     */
1439
    public function getConnectStart(): ?float
1440
    {
1441
        return $this->connectTime;
1✔
1442
    }
1443

1444
    /**
1445
     * Returns the number of seconds with microseconds that it took
1446
     * to connect to the database.
1447
     *
1448
     * Used by the Debug Toolbar's timeline.
1449
     */
1450
    public function getConnectDuration(int $decimals = 6): string
1451
    {
1452
        return number_format($this->connectDuration, $decimals);
2✔
1453
    }
1454

1455
    /**
1456
     * Protect Identifiers
1457
     *
1458
     * This function is used extensively by the Query Builder class, and by
1459
     * a couple functions in this class.
1460
     * It takes a column or table name (optionally with an alias) and inserts
1461
     * the table prefix onto it. Some logic is necessary in order to deal with
1462
     * column names that include the path. Consider a query like this:
1463
     *
1464
     * SELECT hostname.database.table.column AS c FROM hostname.database.table
1465
     *
1466
     * Or a query with aliasing:
1467
     *
1468
     * SELECT m.member_id, m.member_name FROM members AS m
1469
     *
1470
     * Since the column name can include up to four segments (host, DB, table, column)
1471
     * or also have an alias prefix, we need to do a bit of work to figure this out and
1472
     * insert the table prefix (if it exists) in the proper position, and escape only
1473
     * the correct identifiers.
1474
     *
1475
     * @param array|int|string|TableName $item
1476
     * @param bool                       $prefixSingle       Prefix a table name with no segments?
1477
     * @param bool                       $protectIdentifiers Protect table or column names?
1478
     * @param bool                       $fieldExists        Supplied $item contains a column name?
1479
     *
1480
     * @return ($item is array ? array : string)
1481
     */
1482
    public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true)
1483
    {
1484
        if (! is_bool($protectIdentifiers)) {
1,431✔
1485
            $protectIdentifiers = $this->protectIdentifiers;
1,398✔
1486
        }
1487

1488
        if (is_array($item)) {
1,431✔
1489
            $escapedArray = [];
1✔
1490

1491
            foreach ($item as $k => $v) {
1✔
1492
                $escapedArray[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
1✔
1493
            }
1494

1495
            return $escapedArray;
1✔
1496
        }
1497

1498
        if ($item instanceof TableName) {
1,431✔
1499
            /** @psalm-suppress NoValue I don't know why ERROR. */
1500
            return $this->escapeTableName($item);
7✔
1501
        }
1502

1503
        // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int.
1504
        $item = (string) $item;
1,431✔
1505

1506
        // This is basically a bug fix for queries that use MAX, MIN, etc.
1507
        // If a parenthesis is found we know that we do not need to
1508
        // escape the data or add a prefix. There's probably a more graceful
1509
        // way to deal with this, but I'm not thinking of it
1510
        //
1511
        // Added exception for single quotes as well, we don't want to alter
1512
        // literal strings.
1513
        if (strcspn($item, "()'") !== strlen($item)) {
1,431✔
1514
            /** @psalm-suppress NoValue I don't know why ERROR. */
1515
            return $item;
955✔
1516
        }
1517

1518
        // Do not protect identifiers and do not prefix, no swap prefix, there is nothing to do
1519
        if ($protectIdentifiers === false && $prefixSingle === false && $this->swapPre === '') {
1,420✔
1520
            /** @psalm-suppress NoValue I don't know why ERROR. */
1521
            return $item;
117✔
1522
        }
1523

1524
        // Convert tabs or multiple spaces into single spaces
1525
        /** @psalm-suppress NoValue I don't know why ERROR. */
1526
        $item = preg_replace('/\s+/', ' ', trim($item));
1,419✔
1527

1528
        // If the item has an alias declaration we remove it and set it aside.
1529
        // Note: strripos() is used in order to support spaces in table names
1530
        if ($offset = strripos($item, ' AS ')) {
1,419✔
1531
            $alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
11✔
1532
            $item  = substr($item, 0, $offset);
11✔
1533
        } elseif ($offset = strrpos($item, ' ')) {
1,414✔
1534
            $alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
23✔
1535
            $item  = substr($item, 0, $offset);
23✔
1536
        } else {
1537
            $alias = '';
1,406✔
1538
        }
1539

1540
        // Break the string apart if it contains periods, then insert the table prefix
1541
        // in the correct location, assuming the period doesn't indicate that we're dealing
1542
        // with an alias. While we're at it, we will escape the components
1543
        if (str_contains($item, '.')) {
1,419✔
1544
            return $this->protectDotItem($item, $alias, $protectIdentifiers, $fieldExists);
178✔
1545
        }
1546

1547
        // In some cases, especially 'from', we end up running through
1548
        // protect_identifiers twice. This algorithm won't work when
1549
        // it contains the escapeChar so strip it out.
1550
        $item = trim($item, $this->escapeChar);
1,411✔
1551

1552
        // Is there a table prefix? If not, no need to insert it
1553
        if ($this->DBPrefix !== '') {
1,411✔
1554
            // Verify table prefix and replace if necessary
1555
            if ($this->swapPre !== '' && str_starts_with($item, $this->swapPre)) {
987✔
UNCOV
1556
                $item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
×
1557
            }
1558
            // Do we prefix an item with no segments?
1559
            elseif ($prefixSingle && ! str_starts_with($item, $this->DBPrefix)) {
987✔
1560
                $item = $this->DBPrefix . $item;
980✔
1561
            }
1562
        }
1563

1564
        if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers, true)) {
1,411✔
1565
            $item = $this->escapeIdentifiers($item);
1,409✔
1566
        }
1567

1568
        return $item . $alias;
1,411✔
1569
    }
1570

1571
    private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string
1572
    {
1573
        $parts = explode('.', $item);
178✔
1574

1575
        // Does the first segment of the exploded item match
1576
        // one of the aliases previously identified? If so,
1577
        // we have nothing more to do other than escape the item
1578
        //
1579
        // NOTE: The ! empty() condition prevents this method
1580
        // from breaking when QB isn't enabled.
1581
        if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) {
178✔
1582
            if ($protectIdentifiers) {
18✔
1583
                foreach ($parts as $key => $val) {
18✔
1584
                    if (! in_array($val, $this->reservedIdentifiers, true)) {
18✔
1585
                        $parts[$key] = $this->escapeIdentifiers($val);
18✔
1586
                    }
1587
                }
1588

1589
                $item = implode('.', $parts);
18✔
1590
            }
1591

1592
            return $item . $alias;
18✔
1593
        }
1594

1595
        // Is there a table prefix defined in the config file? If not, no need to do anything
1596
        if ($this->DBPrefix !== '') {
167✔
1597
            // We now add the table prefix based on some logic.
1598
            // Do we have 4 segments (hostname.database.table.column)?
1599
            // If so, we add the table prefix to the column name in the 3rd segment.
1600
            if (isset($parts[3])) {
146✔
UNCOV
1601
                $i = 2;
×
1602
            }
1603
            // Do we have 3 segments (database.table.column)?
1604
            // If so, we add the table prefix to the column name in 2nd position
1605
            elseif (isset($parts[2])) {
146✔
UNCOV
1606
                $i = 1;
×
1607
            }
1608
            // Do we have 2 segments (table.column)?
1609
            // If so, we add the table prefix to the column name in 1st segment
1610
            else {
1611
                $i = 0;
146✔
1612
            }
1613

1614
            // This flag is set when the supplied $item does not contain a field name.
1615
            // This can happen when this function is being called from a JOIN.
1616
            if ($fieldExists === false) {
146✔
UNCOV
1617
                $i++;
×
1618
            }
1619

1620
            // Verify table prefix and replace if necessary
1621
            if ($this->swapPre !== '' && str_starts_with($parts[$i], $this->swapPre)) {
146✔
UNCOV
1622
                $parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
×
1623
            }
1624
            // We only add the table prefix if it does not already exist
1625
            elseif (! str_starts_with($parts[$i], $this->DBPrefix)) {
146✔
1626
                $parts[$i] = $this->DBPrefix . $parts[$i];
146✔
1627
            }
1628

1629
            // Put the parts back together
1630
            $item = implode('.', $parts);
146✔
1631
        }
1632

1633
        if ($protectIdentifiers) {
167✔
1634
            $item = $this->escapeIdentifiers($item);
167✔
1635
        }
1636

1637
        return $item . $alias;
167✔
1638
    }
1639

1640
    /**
1641
     * Escape the SQL Identifier
1642
     *
1643
     * This function escapes single identifier.
1644
     *
1645
     * @param non-empty-string|TableName $item
1646
     */
1647
    public function escapeIdentifier($item): string
1648
    {
1649
        if ($item === '') {
869✔
UNCOV
1650
            return '';
×
1651
        }
1652

1653
        if ($item instanceof TableName) {
869✔
1654
            return $this->escapeTableName($item);
8✔
1655
        }
1656

1657
        return $this->escapeChar
869✔
1658
            . str_replace(
869✔
1659
                $this->escapeChar,
869✔
1660
                $this->escapeChar . $this->escapeChar,
869✔
1661
                $item,
869✔
1662
            )
869✔
1663
            . $this->escapeChar;
869✔
1664
    }
1665

1666
    /**
1667
     * Returns escaped table name with alias.
1668
     */
1669
    private function escapeTableName(TableName $tableName): string
1670
    {
1671
        $alias = $tableName->getAlias();
8✔
1672

1673
        return $this->escapeIdentifier($tableName->getActualTableName())
8✔
1674
            . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : '');
8✔
1675
    }
1676

1677
    /**
1678
     * Escape the SQL Identifiers
1679
     *
1680
     * This function escapes column and table names
1681
     *
1682
     * @param array|string $item
1683
     *
1684
     * @return ($item is array ? array : string)
1685
     */
1686
    public function escapeIdentifiers($item)
1687
    {
1688
        if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) {
1,437✔
1689
            return $item;
5✔
1690
        }
1691

1692
        if (is_array($item)) {
1,436✔
1693
            foreach ($item as $key => $value) {
889✔
1694
                $item[$key] = $this->escapeIdentifiers($value);
889✔
1695
            }
1696

1697
            return $item;
889✔
1698
        }
1699

1700
        // Avoid breaking functions and literal values inside queries
1701
        if (ctype_digit($item)
1,436✔
1702
            || $item[0] === "'"
1,435✔
1703
            || ($this->escapeChar !== '"' && $item[0] === '"')
1,435✔
1704
            || str_contains($item, '(')) {
1,436✔
1705
            return $item;
50✔
1706
        }
1707

1708
        if ($this->pregEscapeChar === []) {
1,435✔
1709
            if (is_array($this->escapeChar)) {
524✔
1710
                $this->pregEscapeChar = [
×
1711
                    preg_quote($this->escapeChar[0], '/'),
×
UNCOV
1712
                    preg_quote($this->escapeChar[1], '/'),
×
UNCOV
1713
                    $this->escapeChar[0],
×
UNCOV
1714
                    $this->escapeChar[1],
×
UNCOV
1715
                ];
×
1716
            } else {
1717
                $this->pregEscapeChar[0] = $this->pregEscapeChar[1] = preg_quote($this->escapeChar, '/');
524✔
1718
                $this->pregEscapeChar[2] = $this->pregEscapeChar[3] = $this->escapeChar;
524✔
1719
            }
1720
        }
1721

1722
        foreach ($this->reservedIdentifiers as $id) {
1,435✔
1723
            /** @psalm-suppress NoValue I don't know why ERROR. */
1724
            if (str_contains($item, '.' . $id)) {
1,435✔
1725
                return preg_replace(
3✔
1726
                    '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?\./i',
3✔
1727
                    $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '.',
3✔
1728
                    $item,
3✔
1729
                );
3✔
1730
            }
1731
        }
1732

1733
        /** @psalm-suppress NoValue I don't know why ERROR. */
1734
        return preg_replace(
1,433✔
1735
            '/' . $this->pregEscapeChar[0] . '?([^' . $this->pregEscapeChar[1] . '\.]+)' . $this->pregEscapeChar[1] . '?(\.)?/i',
1,433✔
1736
            $this->pregEscapeChar[2] . '$1' . $this->pregEscapeChar[3] . '$2',
1,433✔
1737
            $item,
1,433✔
1738
        );
1,433✔
1739
    }
1740

1741
    /**
1742
     * Prepends a database prefix if one exists in configuration
1743
     *
1744
     * @throws DatabaseException
1745
     */
1746
    public function prefixTable(string $table = ''): string
1747
    {
1748
        if ($table === '') {
3✔
UNCOV
1749
            throw new DatabaseException('A table name is required for that operation.');
×
1750
        }
1751

1752
        return $this->DBPrefix . $table;
3✔
1753
    }
1754

1755
    /**
1756
     * Returns the total number of rows affected by this query.
1757
     */
1758
    abstract public function affectedRows(): int;
1759

1760
    /**
1761
     * "Smart" Escape String
1762
     *
1763
     * Escapes data based on type.
1764
     * Sets boolean and null types
1765
     *
1766
     * @param mixed $str
1767
     *
1768
     * @return ($str is array ? array : float|int|string)
1769
     */
1770
    public function escape($str)
1771
    {
1772
        if (is_array($str)) {
1,141✔
1773
            return array_map($this->escape(...), $str);
904✔
1774
        }
1775

1776
        if ($str instanceof BackedEnum) {
1,141✔
1777
            $str = $str->value;
9✔
1778
        }
1779

1780
        if ($str instanceof Stringable) {
1,141✔
1781
            if ($str instanceof RawSql) {
13✔
1782
                return $str->__toString();
12✔
1783
            }
1784

1785
            $str = (string) $str;
1✔
1786
        }
1787

1788
        if (is_string($str)) {
1,138✔
1789
            return "'" . $this->escapeString($str) . "'";
1,049✔
1790
        }
1791

1792
        if (is_bool($str)) {
1,046✔
1793
            return ($str === false) ? 0 : 1;
8✔
1794
        }
1795

1796
        return $str ?? 'NULL';
1,044✔
1797
    }
1798

1799
    /**
1800
     * Escape String
1801
     *
1802
     * @param list<string|Stringable>|string|Stringable $str  Input string
1803
     * @param bool                                      $like Whether the string will be used in a LIKE condition
1804
     *
1805
     * @return list<string>|string
1806
     */
1807
    public function escapeString($str, bool $like = false)
1808
    {
1809
        if (is_array($str)) {
1,049✔
1810
            foreach ($str as $key => $val) {
×
UNCOV
1811
                $str[$key] = $this->escapeString($val, $like);
×
1812
            }
1813

UNCOV
1814
            return $str;
×
1815
        }
1816

1817
        if ($str instanceof Stringable) {
1,049✔
1818
            if ($str instanceof RawSql) {
2✔
UNCOV
1819
                return $str->__toString();
×
1820
            }
1821

1822
            $str = (string) $str;
2✔
1823
        }
1824

1825
        $str = $this->_escapeString($str);
1,049✔
1826

1827
        // escape LIKE condition wildcards
1828
        if ($like) {
1,049✔
1829
            return str_replace(
2✔
1830
                [
2✔
1831
                    $this->likeEscapeChar,
2✔
1832
                    '%',
2✔
1833
                    '_',
2✔
1834
                ],
2✔
1835
                [
2✔
1836
                    $this->likeEscapeChar . $this->likeEscapeChar,
2✔
1837
                    $this->likeEscapeChar . '%',
2✔
1838
                    $this->likeEscapeChar . '_',
2✔
1839
                ],
2✔
1840
                $str,
2✔
1841
            );
2✔
1842
        }
1843

1844
        return $str;
1,049✔
1845
    }
1846

1847
    /**
1848
     * Escape LIKE String
1849
     *
1850
     * Calls the individual driver for platform
1851
     * specific escaping for LIKE conditions
1852
     *
1853
     * @param list<string|Stringable>|string|Stringable $str
1854
     *
1855
     * @return list<string>|string
1856
     */
1857
    public function escapeLikeString($str)
1858
    {
1859
        return $this->escapeString($str, true);
2✔
1860
    }
1861

1862
    /**
1863
     * Platform independent string escape.
1864
     *
1865
     * Will likely be overridden in child classes.
1866
     */
1867
    protected function _escapeString(string $str): string
1868
    {
1869
        return str_replace("'", "''", remove_invisible_characters($str, false));
999✔
1870
    }
1871

1872
    /**
1873
     * This function enables you to call PHP database functions that are not natively included
1874
     * in CodeIgniter, in a platform independent manner.
1875
     *
1876
     * @param array ...$params
1877
     *
1878
     * @throws DatabaseException
1879
     */
1880
    public function callFunction(string $functionName, ...$params): bool
1881
    {
1882
        $driver = $this->getDriverFunctionPrefix();
2✔
1883

1884
        if (! str_starts_with($functionName, $driver)) {
2✔
1885
            $functionName = $driver . $functionName;
1✔
1886
        }
1887

1888
        if (! function_exists($functionName)) {
2✔
1889
            if ($this->DBDebug) {
×
UNCOV
1890
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1891
            }
1892

UNCOV
1893
            return false;
×
1894
        }
1895

1896
        return $functionName(...$params);
2✔
1897
    }
1898

1899
    /**
1900
     * Get the prefix of the function to access the DB.
1901
     */
1902
    protected function getDriverFunctionPrefix(): string
1903
    {
UNCOV
1904
        return strtolower($this->DBDriver) . '_';
×
1905
    }
1906

1907
    // --------------------------------------------------------------------
1908
    // META Methods
1909
    // --------------------------------------------------------------------
1910

1911
    /**
1912
     * Returns an array of table names
1913
     *
1914
     * @return false|list<string>
1915
     *
1916
     * @throws DatabaseException
1917
     */
1918
    public function listTables(bool $constrainByPrefix = false)
1919
    {
1920
        if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) {
932✔
1921
            $tables = $constrainByPrefix
926✔
1922
                ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
2✔
1923
                : $this->dataCache['table_names'];
926✔
1924

1925
            return array_values($tables);
926✔
1926
        }
1927

1928
        $sql = $this->_listTables($constrainByPrefix);
111✔
1929

1930
        if ($sql === false) {
111✔
1931
            if ($this->DBDebug) {
×
UNCOV
1932
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1933
            }
1934

UNCOV
1935
            return false;
×
1936
        }
1937

1938
        $this->dataCache['table_names'] = [];
111✔
1939

1940
        $query = $this->query($sql);
111✔
1941

1942
        foreach ($query->getResultArray() as $row) {
111✔
1943
            /** @var string $table */
1944
            $table = $row['table_name'] ?? $row['TABLE_NAME'] ?? $row[array_key_first($row)];
107✔
1945

1946
            $this->dataCache['table_names'][] = $table;
107✔
1947
        }
1948

1949
        return $this->dataCache['table_names'];
111✔
1950
    }
1951

1952
    /**
1953
     * Determine if a particular table exists
1954
     *
1955
     * @param bool $cached Whether to use data cache
1956
     */
1957
    public function tableExists(string $tableName, bool $cached = true): bool
1958
    {
1959
        if ($cached) {
933✔
1960
            return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables(), true);
932✔
1961
        }
1962

1963
        if (false === ($sql = $this->_listTables(false, $tableName))) {
884✔
1964
            if ($this->DBDebug) {
×
UNCOV
1965
                throw new DatabaseException('This feature is not available for the database you are using.');
×
1966
            }
1967

UNCOV
1968
            return false;
×
1969
        }
1970

1971
        $tableExists = $this->query($sql)->getResultArray() !== [];
884✔
1972

1973
        // if cache has been built already
1974
        if (! empty($this->dataCache['table_names'])) {
884✔
1975
            $key = array_search(
880✔
1976
                strtolower($tableName),
880✔
1977
                array_map(strtolower(...), $this->dataCache['table_names']),
880✔
1978
                true,
880✔
1979
            );
880✔
1980

1981
            // table doesn't exist but still in cache - lets reset cache, it can be rebuilt later
1982
            // OR if table does exist but is not found in cache
1983
            if (($key !== false && ! $tableExists) || ($key === false && $tableExists)) {
880✔
1984
                $this->resetDataCache();
1✔
1985
            }
1986
        }
1987

1988
        return $tableExists;
884✔
1989
    }
1990

1991
    /**
1992
     * Fetch Field Names
1993
     *
1994
     * @param string|TableName $tableName
1995
     *
1996
     * @return false|list<string>
1997
     *
1998
     * @throws DatabaseException
1999
     */
2000
    public function getFieldNames($tableName)
2001
    {
2002
        $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
13✔
2003

2004
        // Is there a cached result?
2005
        if (isset($this->dataCache['field_names'][$table])) {
13✔
2006
            return $this->dataCache['field_names'][$table];
3✔
2007
        }
2008

2009
        if (empty($this->connID)) {
13✔
2010
            $this->initialize();
×
2011
        }
2012

2013
        if (false === ($sql = $this->_listColumns($tableName))) {
13✔
2014
            if ($this->DBDebug) {
×
UNCOV
2015
                throw new DatabaseException('This feature is not available for the database you are using.');
×
2016
            }
2017

UNCOV
2018
            return false;
×
2019
        }
2020

2021
        $query = $this->query($sql);
13✔
2022

2023
        $this->dataCache['field_names'][$table] = [];
13✔
2024

2025
        foreach ($query->getResultArray() as $row) {
13✔
2026
            // Do we know from where to get the column's name?
2027
            if (! isset($key)) {
13✔
2028
                if (isset($row['column_name'])) {
13✔
2029
                    $key = 'column_name';
13✔
2030
                } elseif (isset($row['COLUMN_NAME'])) {
13✔
2031
                    $key = 'COLUMN_NAME';
13✔
2032
                } else {
2033
                    // We have no other choice but to just get the first element's key.
2034
                    $key = key($row);
13✔
2035
                }
2036
            }
2037

2038
            $this->dataCache['field_names'][$table][] = $row[$key];
13✔
2039
        }
2040

2041
        return $this->dataCache['field_names'][$table];
13✔
2042
    }
2043

2044
    /**
2045
     * Determine if a particular field exists
2046
     */
2047
    public function fieldExists(string $fieldName, string $tableName): bool
2048
    {
2049
        return in_array($fieldName, $this->getFieldNames($tableName), true);
8✔
2050
    }
2051

2052
    /**
2053
     * Returns an object with field data
2054
     *
2055
     * @return list<stdClass>
2056
     */
2057
    public function getFieldData(string $table)
2058
    {
2059
        return $this->_fieldData($this->protectIdentifiers($table, true, false, false));
160✔
2060
    }
2061

2062
    /**
2063
     * Returns an object with key data
2064
     *
2065
     * @return array<string, stdClass>
2066
     */
2067
    public function getIndexData(string $table)
2068
    {
2069
        return $this->_indexData($this->protectIdentifiers($table, true, false, false));
168✔
2070
    }
2071

2072
    /**
2073
     * Returns an object with foreign key data
2074
     *
2075
     * @return array<string, stdClass>
2076
     */
2077
    public function getForeignKeyData(string $table)
2078
    {
2079
        return $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
37✔
2080
    }
2081

2082
    /**
2083
     * Converts array of arrays generated by _foreignKeyData() to array of objects
2084
     *
2085
     * @return array<string, stdClass>
2086
     *
2087
     * array[
2088
     *    {constraint_name} =>
2089
     *        stdClass[
2090
     *            'constraint_name'     => string,
2091
     *            'table_name'          => string,
2092
     *            'column_name'         => string[],
2093
     *            'foreign_table_name'  => string,
2094
     *            'foreign_column_name' => string[],
2095
     *            'on_delete'           => string,
2096
     *            'on_update'           => string,
2097
     *            'match'               => string
2098
     *        ]
2099
     * ]
2100
     */
2101
    protected function foreignKeyDataToObjects(array $data)
2102
    {
2103
        $retVal = [];
37✔
2104

2105
        foreach ($data as $row) {
37✔
2106
            $name = $row['constraint_name'];
12✔
2107

2108
            // for sqlite generate name
2109
            if ($name === null) {
12✔
2110
                $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign';
11✔
2111
            }
2112

2113
            $obj                      = new stdClass();
12✔
2114
            $obj->constraint_name     = $name;
12✔
2115
            $obj->table_name          = $row['table_name'];
12✔
2116
            $obj->column_name         = $row['column_name'];
12✔
2117
            $obj->foreign_table_name  = $row['foreign_table_name'];
12✔
2118
            $obj->foreign_column_name = $row['foreign_column_name'];
12✔
2119
            $obj->on_delete           = $row['on_delete'];
12✔
2120
            $obj->on_update           = $row['on_update'];
12✔
2121
            $obj->match               = $row['match'];
12✔
2122

2123
            $retVal[$name] = $obj;
12✔
2124
        }
2125

2126
        return $retVal;
37✔
2127
    }
2128

2129
    /**
2130
     * Disables foreign key checks temporarily.
2131
     *
2132
     * @return bool
2133
     */
2134
    public function disableForeignKeyChecks()
2135
    {
2136
        $sql = $this->_disableForeignKeyChecks();
898✔
2137

2138
        if ($sql === '') {
898✔
2139
            // The feature is not supported.
UNCOV
2140
            return false;
×
2141
        }
2142

2143
        return $this->query($sql);
898✔
2144
    }
2145

2146
    /**
2147
     * Enables foreign key checks temporarily.
2148
     *
2149
     * @return bool
2150
     */
2151
    public function enableForeignKeyChecks()
2152
    {
2153
        $sql = $this->_enableForeignKeyChecks();
975✔
2154

2155
        if ($sql === '') {
975✔
2156
            // The feature is not supported.
UNCOV
2157
            return false;
×
2158
        }
2159

2160
        return $this->query($sql);
975✔
2161
    }
2162

2163
    /**
2164
     * Allows the engine to be set into a mode where queries are not
2165
     * actually executed, but they are still generated, timed, etc.
2166
     *
2167
     * This is primarily used by the prepared query functionality.
2168
     *
2169
     * @return $this
2170
     */
2171
    public function pretend(bool $pretend = true)
2172
    {
2173
        $this->pretend = $pretend;
19✔
2174

2175
        return $this;
19✔
2176
    }
2177

2178
    /**
2179
     * Empties our data cache. Especially helpful during testing.
2180
     *
2181
     * @return $this
2182
     */
2183
    public function resetDataCache()
2184
    {
2185
        $this->dataCache = [];
51✔
2186

2187
        return $this;
51✔
2188
    }
2189

2190
    /**
2191
     * Determines if the statement is a write-type query or not.
2192
     *
2193
     * @param string $sql
2194
     */
2195
    public function isWriteType($sql): bool
2196
    {
2197
        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);
1,007✔
2198
    }
2199

2200
    /**
2201
     * Returns the last error code and message.
2202
     *
2203
     * Must return an array with keys 'code' and 'message':
2204
     *
2205
     * @return array{code: int|string|null, message: string|null}
2206
     */
2207
    abstract public function error(): array;
2208

2209
    /**
2210
     * Returns the exception that would have been thrown on the last failed
2211
     * query if DBDebug were enabled. Returns null if the last query succeeded
2212
     * or if DBDebug is true (in which case the exception is always thrown
2213
     * directly and this method will always return null).
2214
     */
2215
    public function getLastException(): ?DatabaseException
2216
    {
2217
        return $this->lastException;
8✔
2218
    }
2219

2220
    /**
2221
     * Sets the exception for the last failed database operation.
2222
     *
2223
     * @internal This method is for internal database component use only.
2224
     */
2225
    public function setLastException(?DatabaseException $exception): void
2226
    {
2227
        $this->lastException = $exception;
20✔
2228
    }
2229

2230
    /**
2231
     * Checks whether the native database error represents a unique constraint violation.
2232
     */
2233
    protected function isUniqueConstraintViolation(int|string $code, string $message): bool
2234
    {
2235
        return false;
8✔
2236
    }
2237

2238
    /**
2239
     * Checks whether the native database error represents a foreign key constraint violation.
2240
     */
2241
    protected function isForeignKeyConstraintViolation(int|string $code, string $message): bool
2242
    {
2243
        return false;
69✔
2244
    }
2245

2246
    /**
2247
     * Checks whether the native database error represents a NOT NULL constraint violation.
2248
     */
2249
    protected function isNotNullConstraintViolation(int|string $code, string $message): bool
2250
    {
2251
        return false;
8✔
2252
    }
2253

2254
    /**
2255
     * Checks whether the native database error represents a CHECK constraint violation.
2256
     */
2257
    protected function isCheckConstraintViolation(int|string $code, string $message): bool
2258
    {
2259
        return false;
62✔
2260
    }
2261

2262
    /**
2263
     * Checks whether the native database error represents a constraint violation.
2264
     */
2265
    protected function isConstraintViolation(int|string $code, string $message): bool
2266
    {
2267
        return false;
31✔
2268
    }
2269

2270
    /**
2271
     * Checks whether the native database code represents a retryable transaction failure.
2272
     */
2273
    protected function isRetryableTransactionErrorCode(int|string $code): bool
2274
    {
2275
        return false;
3✔
2276
    }
2277

2278
    /**
2279
     * Creates the appropriate database exception for a native database error.
2280
     *
2281
     * @internal This method is for internal database component use only.
2282
     */
2283
    public function createDatabaseException(
2284
        string $message,
2285
        int|string $code = 0,
2286
        ?Throwable $previous = null,
2287
    ): DatabaseException {
2288
        if ($this->isUniqueConstraintViolation($code, $message)) {
117✔
2289
            return new UniqueConstraintViolationException($message, $code, $previous);
44✔
2290
        }
2291

2292
        if ($this->isForeignKeyConstraintViolation($code, $message)) {
99✔
2293
            return new ForeignKeyConstraintViolationException($message, $code, $previous);
7✔
2294
        }
2295

2296
        if ($this->isNotNullConstraintViolation($code, $message)) {
93✔
2297
            return new NotNullConstraintViolationException($message, $code, $previous);
11✔
2298
        }
2299

2300
        if ($this->isCheckConstraintViolation($code, $message)) {
83✔
2301
            return new CheckConstraintViolationException($message, $code, $previous);
4✔
2302
        }
2303

2304
        if ($this->isConstraintViolation($code, $message)) {
79✔
2305
            return new ConstraintViolationException($message, $code, $previous);
32✔
2306
        }
2307

2308
        if ($this->isRetryableTransactionErrorCode($code)) {
47✔
2309
            return new RetryableTransactionException($message, $code, $previous);
16✔
2310
        }
2311

2312
        return new DatabaseException($message, $code, $previous);
31✔
2313
    }
2314

2315
    /**
2316
     * Insert ID
2317
     *
2318
     * @return int|string
2319
     */
2320
    abstract public function insertID();
2321

2322
    /**
2323
     * Generates the SQL for listing tables in a platform-dependent manner.
2324
     *
2325
     * @param string|null $tableName If $tableName is provided will return only this table if exists.
2326
     *
2327
     * @return false|string
2328
     */
2329
    abstract protected function _listTables(bool $constrainByPrefix = false, ?string $tableName = null);
2330

2331
    /**
2332
     * Generates a platform-specific query string so that the column names can be fetched.
2333
     *
2334
     * @param string|TableName $table
2335
     *
2336
     * @return false|string
2337
     */
2338
    abstract protected function _listColumns($table = '');
2339

2340
    /**
2341
     * Platform-specific field data information.
2342
     *
2343
     * @see getFieldData()
2344
     *
2345
     * @return list<stdClass>
2346
     */
2347
    abstract protected function _fieldData(string $table): array;
2348

2349
    /**
2350
     * Platform-specific index data.
2351
     *
2352
     * @see    getIndexData()
2353
     *
2354
     * @return array<string, stdClass>
2355
     */
2356
    abstract protected function _indexData(string $table): array;
2357

2358
    /**
2359
     * Platform-specific foreign keys data.
2360
     *
2361
     * @see    getForeignKeyData()
2362
     *
2363
     * @return array<string, stdClass>
2364
     */
2365
    abstract protected function _foreignKeyData(string $table): array;
2366

2367
    /**
2368
     * Platform-specific SQL statement to disable foreign key checks.
2369
     *
2370
     * If this feature is not supported, return empty string.
2371
     *
2372
     * @TODO This method should be moved to an interface that represents foreign key support.
2373
     *
2374
     * @return string
2375
     *
2376
     * @see disableForeignKeyChecks()
2377
     */
2378
    protected function _disableForeignKeyChecks()
2379
    {
UNCOV
2380
        return '';
×
2381
    }
2382

2383
    /**
2384
     * Platform-specific SQL statement to enable foreign key checks.
2385
     *
2386
     * If this feature is not supported, return empty string.
2387
     *
2388
     * @TODO This method should be moved to an interface that represents foreign key support.
2389
     *
2390
     * @return string
2391
     *
2392
     * @see enableForeignKeyChecks()
2393
     */
2394
    protected function _enableForeignKeyChecks()
2395
    {
UNCOV
2396
        return '';
×
2397
    }
2398

2399
    /**
2400
     * Converts a named timezone to an offset string.
2401
     *
2402
     * Converts timezone identifiers (e.g., 'America/New_York') to offset strings
2403
     * (e.g., '-05:00' or '-04:00' depending on DST). This is useful because not all
2404
     * databases have timezone tables loaded, but all support offset notation.
2405
     *
2406
     * @param string $timezone Named timezone (e.g., 'America/New_York', 'UTC', 'Europe/Paris')
2407
     *
2408
     * @return string Offset string (e.g., '+00:00', '-05:00', '+01:00')
2409
     */
2410
    protected function convertTimezoneToOffset(string $timezone): string
2411
    {
2412
        // If it's already an offset, return as-is
2413
        if (preg_match('/^[+-]\d{2}:\d{2}$/', $timezone)) {
9✔
2414
            return $timezone;
3✔
2415
        }
2416

2417
        try {
2418
            $offset = Time::now($timezone)->getOffset();
6✔
2419

2420
            // Convert offset seconds to +-HH:MM format
2421
            $hours   = (int) ($offset / 3600);
5✔
2422
            $minutes = abs((int) (($offset % 3600) / 60));
5✔
2423

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

2429
            return '+00:00';
1✔
2430
        }
2431
    }
2432

2433
    /**
2434
     * Gets the timezone string to use for database session.
2435
     *
2436
     * Handles the timezone configuration logic:
2437
     * - false: Don't set timezone (returns null)
2438
     * - true: Auto-sync with app timezone from config
2439
     * - string: Use specific timezone (converts named timezones to offsets)
2440
     *
2441
     * @return string|null The timezone offset string, or null if timezone should not be set
2442
     */
2443
    protected function getSessionTimezone(): ?string
2444
    {
2445
        if ($this->timezone === false) {
85✔
2446
            return null;
79✔
2447
        }
2448

2449
        // Auto-sync with app timezone
2450
        if ($this->timezone === true) {
6✔
2451
            $appConfig = config('App');
2✔
2452
            $timezone  = $appConfig->appTimezone;
2✔
2453
        } else {
2454
            // Use specific timezone from config
2455
            $timezone = $this->timezone;
4✔
2456
        }
2457

2458
        return $this->convertTimezoneToOffset($timezone);
6✔
2459
    }
2460

2461
    /**
2462
     * Accessor for properties if they exist.
2463
     *
2464
     * @return mixed
2465
     */
2466
    public function __get(string $key)
2467
    {
2468
        if (property_exists($this, $key)) {
1,360✔
2469
            return $this->{$key};
1,359✔
2470
        }
2471

2472
        return null;
1✔
2473
    }
2474

2475
    /**
2476
     * Checker for properties existence.
2477
     */
2478
    public function __isset(string $key): bool
2479
    {
2480
        return property_exists($this, $key);
380✔
2481
    }
2482
}
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