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

codeigniter4 / CodeIgniter4 / 8677009716

13 Apr 2024 11:45PM UTC coverage: 84.44% (-2.2%) from 86.607%
8677009716

push

github

web-flow
Merge pull request #8776 from kenjis/fix-findQualifiedNameFromPath-Cannot-declare-class-v3

fix: Cannot declare class CodeIgniter\Config\Services, because the name is already in use

0 of 3 new or added lines in 1 file covered. (0.0%)

478 existing lines in 72 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

89.31
/system/Database/Forge.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 CodeIgniter\Database\Exceptions\DatabaseException;
17
use InvalidArgumentException;
18
use RuntimeException;
19
use Throwable;
20

21
/**
22
 * The Forge class transforms migrations to executable
23
 * SQL statements.
24
 */
25
class Forge
26
{
27
    /**
28
     * The active database connection.
29
     *
30
     * @var BaseConnection
31
     */
32
    protected $db;
33

34
    /**
35
     * List of fields.
36
     *
37
     * @var array<string, array|string> [name => attributes]
38
     */
39
    protected $fields = [];
40

41
    /**
42
     * List of keys.
43
     *
44
     * @var list<array{fields?: list<string>, keyName?: string}>
45
     */
46
    protected $keys = [];
47

48
    /**
49
     * List of unique keys.
50
     *
51
     * @var array
52
     */
53
    protected $uniqueKeys = [];
54

55
    /**
56
     * Primary keys.
57
     *
58
     * @var array{fields?: list<string>, keyName?: string}
59
     */
60
    protected $primaryKeys = [];
61

62
    /**
63
     * List of foreign keys.
64
     *
65
     * @var array
66
     */
67
    protected $foreignKeys = [];
68

69
    /**
70
     * Character set used.
71
     *
72
     * @var string
73
     */
74
    protected $charset = '';
75

76
    /**
77
     * CREATE DATABASE statement
78
     *
79
     * @var false|string
80
     */
81
    protected $createDatabaseStr = 'CREATE DATABASE %s';
82

83
    /**
84
     * CREATE DATABASE IF statement
85
     *
86
     * @var string
87
     */
88
    protected $createDatabaseIfStr;
89

90
    /**
91
     * CHECK DATABASE EXIST statement
92
     *
93
     * @var string
94
     */
95
    protected $checkDatabaseExistStr;
96

97
    /**
98
     * DROP DATABASE statement
99
     *
100
     * @var false|string
101
     */
102
    protected $dropDatabaseStr = 'DROP DATABASE %s';
103

104
    /**
105
     * CREATE TABLE statement
106
     *
107
     * @var string
108
     */
109
    protected $createTableStr = "%s %s (%s\n)";
110

111
    /**
112
     * CREATE TABLE IF statement
113
     *
114
     * @var bool|string
115
     *
116
     * @deprecated This is no longer used.
117
     */
118
    protected $createTableIfStr = 'CREATE TABLE IF NOT EXISTS';
119

120
    /**
121
     * CREATE TABLE keys flag
122
     *
123
     * Whether table keys are created from within the
124
     * CREATE TABLE statement.
125
     *
126
     * @var bool
127
     */
128
    protected $createTableKeys = false;
129

130
    /**
131
     * DROP TABLE IF EXISTS statement
132
     *
133
     * @var bool|string
134
     */
135
    protected $dropTableIfStr = 'DROP TABLE IF EXISTS';
136

137
    /**
138
     * RENAME TABLE statement
139
     *
140
     * @var false|string
141
     */
142
    protected $renameTableStr = 'ALTER TABLE %s RENAME TO %s';
143

144
    /**
145
     * UNSIGNED support
146
     *
147
     * @var array|bool
148
     */
149
    protected $unsigned = true;
150

151
    /**
152
     * NULL value representation in CREATE/ALTER TABLE statements
153
     *
154
     * @var string
155
     *
156
     * @internal Used for marking nullable fields. Not covered by BC promise.
157
     */
158
    protected $null = 'NULL';
159

160
    /**
161
     * DEFAULT value representation in CREATE/ALTER TABLE statements
162
     *
163
     * @var false|string
164
     */
165
    protected $default = ' DEFAULT ';
166

167
    /**
168
     * DROP CONSTRAINT statement
169
     *
170
     * @var string
171
     */
172
    protected $dropConstraintStr;
173

174
    /**
175
     * DROP INDEX statement
176
     *
177
     * @var string
178
     */
179
    protected $dropIndexStr = 'DROP INDEX %s ON %s';
180

181
    /**
182
     * Foreign Key Allowed Actions
183
     *
184
     * @var array
185
     */
186
    protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
187

188
    /**
189
     * Constructor.
190
     */
191
    public function __construct(BaseConnection $db)
192
    {
193
        $this->db = $db;
691✔
194
    }
195

196
    /**
197
     * Provides access to the forge's current database connection.
198
     *
199
     * @return ConnectionInterface
200
     */
201
    public function getConnection()
202
    {
203
        return $this->db;
611✔
204
    }
205

206
    /**
207
     * Create database
208
     *
209
     * @param bool $ifNotExists Whether to add IF NOT EXISTS condition
210
     *
211
     * @throws DatabaseException
212
     */
213
    public function createDatabase(string $dbName, bool $ifNotExists = false): bool
214
    {
215
        if ($ifNotExists && $this->createDatabaseIfStr === null) {
8✔
216
            if ($this->databaseExists($dbName)) {
3✔
217
                return true;
2✔
218
            }
219

220
            $ifNotExists = false;
1✔
221
        }
222

223
        if ($this->createDatabaseStr === false) {
8✔
224
            if ($this->db->DBDebug) {
1✔
225
                throw new DatabaseException('This feature is not available for the database you are using.');
1✔
226
            }
227

UNCOV
228
            return false; // @codeCoverageIgnore
×
229
        }
230

231
        try {
232
            if (! $this->db->query(
7✔
233
                sprintf(
7✔
234
                    $ifNotExists ? $this->createDatabaseIfStr : $this->createDatabaseStr,
7✔
235
                    $this->db->escapeIdentifier($dbName),
7✔
236
                    $this->db->charset,
7✔
237
                    $this->db->DBCollat
7✔
238
                )
7✔
239
            )) {
7✔
240
                // @codeCoverageIgnoreStart
UNCOV
241
                if ($this->db->DBDebug) {
×
UNCOV
242
                    throw new DatabaseException('Unable to create the specified database.');
×
243
                }
244

UNCOV
245
                return false;
×
246
                // @codeCoverageIgnoreEnd
247
            }
248

249
            if (! empty($this->db->dataCache['db_names'])) {
7✔
250
                $this->db->dataCache['db_names'][] = $dbName;
7✔
251
            }
252

253
            return true;
7✔
254
        } catch (Throwable $e) {
1✔
255
            if ($this->db->DBDebug) {
1✔
256
                throw new DatabaseException('Unable to create the specified database.', 0, $e);
1✔
257
            }
258

UNCOV
259
            return false; // @codeCoverageIgnore
×
260
        }
261
    }
262

263
    /**
264
     * Determine if a database exists
265
     *
266
     * @throws DatabaseException
267
     */
268
    private function databaseExists(string $dbName): bool
269
    {
270
        if ($this->checkDatabaseExistStr === null) {
3✔
271
            if ($this->db->DBDebug) {
×
272
                throw new DatabaseException('This feature is not available for the database you are using.');
×
273
            }
274

275
            return false;
×
276
        }
277

278
        return $this->db->query($this->checkDatabaseExistStr, $dbName)->getRow() !== null;
3✔
279
    }
280

281
    /**
282
     * Drop database
283
     *
284
     * @throws DatabaseException
285
     */
286
    public function dropDatabase(string $dbName): bool
287
    {
288
        if ($this->dropDatabaseStr === false) {
8✔
289
            if ($this->db->DBDebug) {
1✔
290
                throw new DatabaseException('This feature is not available for the database you are using.');
1✔
291
            }
292

293
            return false;
×
294
        }
295

296
        if (! $this->db->query(
7✔
297
            sprintf($this->dropDatabaseStr, $this->db->escapeIdentifier($dbName))
7✔
298
        )) {
7✔
299
            if ($this->db->DBDebug) {
×
300
                throw new DatabaseException('Unable to drop the specified database.');
×
301
            }
302

303
            return false;
×
304
        }
305

306
        if (! empty($this->db->dataCache['db_names'])) {
7✔
307
            $key = array_search(
5✔
308
                strtolower($dbName),
5✔
309
                array_map('strtolower', $this->db->dataCache['db_names']),
5✔
310
                true
5✔
311
            );
5✔
312
            if ($key !== false) {
5✔
313
                unset($this->db->dataCache['db_names'][$key]);
5✔
314
            }
315
        }
316

317
        return true;
7✔
318
    }
319

320
    /**
321
     * Add Key
322
     *
323
     * @param array|string $key
324
     *
325
     * @return Forge
326
     */
327
    public function addKey($key, bool $primary = false, bool $unique = false, string $keyName = '')
328
    {
329
        if ($primary) {
612✔
330
            $this->primaryKeys = ['fields' => (array) $key, 'keyName' => $keyName];
612✔
331
        } else {
332
            $this->keys[] = ['fields' => (array) $key, 'keyName' => $keyName];
608✔
333

334
            if ($unique) {
608✔
335
                $this->uniqueKeys[] = count($this->keys) - 1;
607✔
336
            }
337
        }
338

339
        return $this;
612✔
340
    }
341

342
    /**
343
     * Add Primary Key
344
     *
345
     * @param array|string $key
346
     *
347
     * @return Forge
348
     */
349
    public function addPrimaryKey($key, string $keyName = '')
350
    {
351
        return $this->addKey($key, true, false, $keyName);
24✔
352
    }
353

354
    /**
355
     * Add Unique Key
356
     *
357
     * @param array|string $key
358
     *
359
     * @return Forge
360
     */
361
    public function addUniqueKey($key, string $keyName = '')
362
    {
363
        return $this->addKey($key, false, true, $keyName);
607✔
364
    }
365

366
    /**
367
     * Add Field
368
     *
369
     * @param array<string, array|string>|string $fields Field array or Field string
370
     *
371
     * @return Forge
372
     */
373
    public function addField($fields)
374
    {
375
        if (is_string($fields)) {
620✔
376
            if ($fields === 'id') {
593✔
377
                $this->addField([
8✔
378
                    'id' => [
8✔
379
                        'type'           => 'INT',
8✔
380
                        'constraint'     => 9,
8✔
381
                        'auto_increment' => true,
8✔
382
                    ],
8✔
383
                ]);
8✔
384
                $this->addKey('id', true);
8✔
385
            } else {
386
                if (! str_contains($fields, ' ')) {
592✔
387
                    throw new InvalidArgumentException('Field information is required for that operation.');
1✔
388
                }
389

390
                $fieldName = explode(' ', $fields, 2)[0];
592✔
391
                $fieldName = trim($fieldName, '`\'"');
592✔
392

393
                $this->fields[$fieldName] = $fields;
592✔
394
            }
395
        }
396

397
        if (is_array($fields)) {
620✔
398
            foreach ($fields as $name => $attributes) {
620✔
399
                if (is_string($attributes)) {
620✔
400
                    $this->addField($attributes);
592✔
401

402
                    continue;
592✔
403
                }
404

405
                if (is_array($attributes)) {
620✔
406
                    $this->fields = array_merge($this->fields, [$name => $attributes]);
620✔
407
                }
408
            }
409
        }
410

411
        return $this;
620✔
412
    }
413

414
    /**
415
     * Add Foreign Key
416
     *
417
     * @param list<string>|string $fieldName
418
     * @param list<string>|string $tableField
419
     *
420
     * @throws DatabaseException
421
     */
422
    public function addForeignKey(
423
        $fieldName = '',
424
        string $tableName = '',
425
        $tableField = '',
426
        string $onUpdate = '',
427
        string $onDelete = '',
428
        string $fkName = ''
429
    ): Forge {
430
        $fieldName  = (array) $fieldName;
14✔
431
        $tableField = (array) $tableField;
14✔
432

433
        $this->foreignKeys[] = [
14✔
434
            'field'          => $fieldName,
14✔
435
            'referenceTable' => $tableName,
14✔
436
            'referenceField' => $tableField,
14✔
437
            'onDelete'       => strtoupper($onDelete),
14✔
438
            'onUpdate'       => strtoupper($onUpdate),
14✔
439
            'fkName'         => $fkName,
14✔
440
        ];
14✔
441

442
        return $this;
14✔
443
    }
444

445
    /**
446
     * Drop Key
447
     *
448
     * @throws DatabaseException
449
     */
450
    public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool
451
    {
452
        $keyName = $this->db->escapeIdentifiers(($prefixKeyName === true ? $this->db->DBPrefix : '') . $keyName);
2✔
453
        $table   = $this->db->escapeIdentifiers($this->db->DBPrefix . $table);
2✔
454

455
        $dropKeyAsConstraint = $this->dropKeyAsConstraint($table, $keyName);
2✔
456

457
        if ($dropKeyAsConstraint === true) {
2✔
458
            $sql = sprintf(
1✔
459
                $this->dropConstraintStr,
1✔
460
                $table,
1✔
461
                $keyName,
1✔
462
            );
1✔
463
        } else {
464
            $sql = sprintf(
2✔
465
                $this->dropIndexStr,
2✔
466
                $keyName,
2✔
467
                $table,
2✔
468
            );
2✔
469
        }
470

471
        if ($sql === '') {
2✔
472
            if ($this->db->DBDebug) {
×
473
                throw new DatabaseException('This feature is not available for the database you are using.');
×
474
            }
475

476
            return false;
×
477
        }
478

479
        return $this->db->query($sql);
2✔
480
    }
481

482
    /**
483
     * Checks if key needs to be dropped as a constraint.
484
     */
485
    protected function dropKeyAsConstraint(string $table, string $constraintName): bool
486
    {
487
        $sql = $this->_dropKeyAsConstraint($table, $constraintName);
2✔
488

489
        if ($sql === '') {
2✔
490
            return false;
2✔
491
        }
492

493
        return $this->db->query($sql)->getResultArray() !== [];
2✔
494
    }
495

496
    /**
497
     * Constructs sql to check if key is a constraint.
498
     */
499
    protected function _dropKeyAsConstraint(string $table, string $constraintName): string
500
    {
501
        return '';
2✔
502
    }
503

504
    /**
505
     * Drop Primary Key
506
     */
507
    public function dropPrimaryKey(string $table, string $keyName = ''): bool
508
    {
509
        $sql = sprintf(
2✔
510
            'ALTER TABLE %s DROP CONSTRAINT %s',
2✔
511
            $this->db->escapeIdentifiers($this->db->DBPrefix . $table),
2✔
512
            ($keyName === '') ? $this->db->escapeIdentifiers('pk_' . $this->db->DBPrefix . $table) : $this->db->escapeIdentifiers($keyName),
2✔
513
        );
2✔
514

515
        return $this->db->query($sql);
2✔
516
    }
517

518
    /**
519
     * @return bool
520
     *
521
     * @throws DatabaseException
522
     */
523
    public function dropForeignKey(string $table, string $foreignName)
524
    {
525
        $sql = sprintf(
2✔
526
            (string) $this->dropConstraintStr,
2✔
527
            $this->db->escapeIdentifiers($this->db->DBPrefix . $table),
2✔
528
            $this->db->escapeIdentifiers($foreignName)
2✔
529
        );
2✔
530

531
        if ($sql === '') {
2✔
532
            if ($this->db->DBDebug) {
1✔
533
                throw new DatabaseException('This feature is not available for the database you are using.');
1✔
534
            }
535

536
            return false;
×
537
        }
538

539
        return $this->db->query($sql);
1✔
540
    }
541

542
    /**
543
     * @param array $attributes Table attributes
544
     *
545
     * @return bool
546
     *
547
     * @throws DatabaseException
548
     */
549
    public function createTable(string $table, bool $ifNotExists = false, array $attributes = [])
550
    {
551
        if ($table === '') {
620✔
552
            throw new InvalidArgumentException('A table name is required for that operation.');
1✔
553
        }
554

555
        $table = $this->db->DBPrefix . $table;
620✔
556

557
        if ($this->fields === []) {
620✔
558
            throw new RuntimeException('Field information is required.');
1✔
559
        }
560

561
        // If table exists lets stop here
562
        if ($ifNotExists === true && $this->db->tableExists($table, false)) {
620✔
563
            $this->reset();
3✔
564

565
            return true;
3✔
566
        }
567

568
        $sql = $this->_createTable($table, false, $attributes);
619✔
569

570
        if (($result = $this->db->query($sql)) !== false) {
619✔
571
            if (isset($this->db->dataCache['table_names']) && ! in_array($table, $this->db->dataCache['table_names'], true)) {
619✔
572
                $this->db->dataCache['table_names'][] = $table;
612✔
573
            }
574

575
            // Most databases don't support creating indexes from within the CREATE TABLE statement
576
            if (! empty($this->keys)) {
619✔
577
                for ($i = 0, $sqls = $this->_processIndexes($table), $c = count($sqls); $i < $c; $i++) {
588✔
578
                    $this->db->query($sqls[$i]);
588✔
579
                }
580
            }
581
        }
582

583
        $this->reset();
619✔
584

585
        return $result;
619✔
586
    }
587

588
    /**
589
     * @param array $attributes Table attributes
590
     *
591
     * @return string SQL string
592
     *
593
     * @deprecated $ifNotExists is no longer used, and will be removed.
594
     */
595
    protected function _createTable(string $table, bool $ifNotExists, array $attributes)
596
    {
597
        $processedFields = $this->_processFields(true);
619✔
598

599
        for ($i = 0, $c = count($processedFields); $i < $c; $i++) {
619✔
600
            $processedFields[$i] = ($processedFields[$i]['_literal'] !== false) ? "\n\t" . $processedFields[$i]['_literal']
619✔
601
                : "\n\t" . $this->_processColumn($processedFields[$i]);
619✔
602
        }
603

604
        $processedFields = implode(',', $processedFields);
619✔
605

606
        $processedFields .= $this->_processPrimaryKeys($table);
619✔
607
        $processedFields .= current($this->_processForeignKeys($table));
619✔
608

609
        if ($this->createTableKeys === true) {
619✔
610
            $indexes = current($this->_processIndexes($table));
593✔
611
            if (is_string($indexes)) {
593✔
612
                $processedFields .= $indexes;
593✔
613
            }
614
        }
615

616
        return sprintf(
619✔
617
            $this->createTableStr . '%s',
619✔
618
            'CREATE TABLE',
619✔
619
            $this->db->escapeIdentifiers($table),
619✔
620
            $processedFields,
619✔
621
            $this->_createTableAttributes($attributes)
619✔
622
        );
619✔
623
    }
624

625
    protected function _createTableAttributes(array $attributes): string
626
    {
627
        $sql = '';
593✔
628

629
        foreach (array_keys($attributes) as $key) {
593✔
630
            if (is_string($key)) {
×
631
                $sql .= ' ' . strtoupper($key) . ' ' . $this->db->escape($attributes[$key]);
×
632
            }
633
        }
634

635
        return $sql;
593✔
636
    }
637

638
    /**
639
     * @return bool
640
     *
641
     * @throws DatabaseException
642
     */
643
    public function dropTable(string $tableName, bool $ifExists = false, bool $cascade = false)
644
    {
645
        if ($tableName === '') {
617✔
646
            if ($this->db->DBDebug) {
1✔
647
                throw new DatabaseException('A table name is required for that operation.');
1✔
648
            }
649

650
            return false;
×
651
        }
652

653
        if ($this->db->DBPrefix && str_starts_with($tableName, $this->db->DBPrefix)) {
617✔
654
            $tableName = substr($tableName, strlen($this->db->DBPrefix));
1✔
655
        }
656

657
        if (($query = $this->_dropTable($this->db->DBPrefix . $tableName, $ifExists, $cascade)) === true) {
617✔
658
            return true;
54✔
659
        }
660

661
        $this->db->disableForeignKeyChecks();
617✔
662

663
        $query = $this->db->query($query);
617✔
664

665
        $this->db->enableForeignKeyChecks();
617✔
666

667
        if ($query && ! empty($this->db->dataCache['table_names'])) {
617✔
668
            $key = array_search(
608✔
669
                strtolower($this->db->DBPrefix . $tableName),
608✔
670
                array_map('strtolower', $this->db->dataCache['table_names']),
608✔
671
                true
608✔
672
            );
608✔
673

674
            if ($key !== false) {
608✔
675
                unset($this->db->dataCache['table_names'][$key]);
607✔
676
            }
677
        }
678

679
        return $query;
617✔
680
    }
681

682
    /**
683
     * Generates a platform-specific DROP TABLE string
684
     *
685
     * @return bool|string
686
     */
687
    protected function _dropTable(string $table, bool $ifExists, bool $cascade)
688
    {
689
        $sql = 'DROP TABLE';
617✔
690

691
        if ($ifExists) {
617✔
692
            if ($this->dropTableIfStr === false) {
617✔
693
                if (! $this->db->tableExists($table)) {
573✔
694
                    return true;
54✔
695
                }
696
            } else {
697
                $sql = sprintf($this->dropTableIfStr, $this->db->escapeIdentifiers($table));
609✔
698
            }
699
        }
700

701
        return $sql . ' ' . $this->db->escapeIdentifiers($table);
617✔
702
    }
703

704
    /**
705
     * @return bool
706
     *
707
     * @throws DatabaseException
708
     */
709
    public function renameTable(string $tableName, string $newTableName)
710
    {
711
        if ($tableName === '' || $newTableName === '') {
33✔
712
            throw new InvalidArgumentException('A table name is required for that operation.');
1✔
713
        }
714

715
        if ($this->renameTableStr === false) {
32✔
716
            if ($this->db->DBDebug) {
1✔
717
                throw new DatabaseException('This feature is not available for the database you are using.');
1✔
718
            }
719

720
            return false;
×
721
        }
722

723
        $result = $this->db->query(sprintf(
31✔
724
            $this->renameTableStr,
31✔
725
            $this->db->escapeIdentifiers($this->db->DBPrefix . $tableName),
31✔
726
            $this->db->escapeIdentifiers($this->db->DBPrefix . $newTableName)
31✔
727
        ));
31✔
728

729
        if ($result && ! empty($this->db->dataCache['table_names'])) {
31✔
730
            $key = array_search(
31✔
731
                strtolower($this->db->DBPrefix . $tableName),
31✔
732
                array_map('strtolower', $this->db->dataCache['table_names']),
31✔
733
                true
31✔
734
            );
31✔
735

736
            if ($key !== false) {
31✔
737
                $this->db->dataCache['table_names'][$key] = $this->db->DBPrefix . $newTableName;
31✔
738
            }
739
        }
740

741
        return $result;
31✔
742
    }
743

744
    /**
745
     * @param array<string, array|string>|string $fields Field array or Field string
746
     *
747
     * @throws DatabaseException
748
     */
749
    public function addColumn(string $table, $fields): bool
750
    {
751
        // Work-around for literal column definitions
752
        if (is_string($fields)) {
15✔
753
            $fields = [$fields];
×
754
        }
755

756
        foreach (array_keys($fields) as $name) {
15✔
757
            $this->addField([$name => $fields[$name]]);
15✔
758
        }
759

760
        $sqls = $this->_alterTable('ADD', $this->db->DBPrefix . $table, $this->_processFields());
15✔
761
        $this->reset();
15✔
762

763
        if ($sqls === false) {
15✔
764
            if ($this->db->DBDebug) {
×
765
                throw new DatabaseException('This feature is not available for the database you are using.');
×
766
            }
767

768
            return false;
×
769
        }
770

771
        foreach ($sqls as $sql) {
15✔
772
            if ($this->db->query($sql) === false) {
15✔
773
                return false;
×
774
            }
775
        }
776

777
        return true;
15✔
778
    }
779

780
    /**
781
     * @param array|string $columnNames column names to DROP
782
     *
783
     * @return bool
784
     *
785
     * @throws DatabaseException
786
     */
787
    public function dropColumn(string $table, $columnNames)
788
    {
789
        $sql = $this->_alterTable('DROP', $this->db->DBPrefix . $table, $columnNames);
15✔
790

791
        if ($sql === false) {
15✔
792
            if ($this->db->DBDebug) {
×
793
                throw new DatabaseException('This feature is not available for the database you are using.');
×
794
            }
795

796
            return false;
×
797
        }
798

799
        return $this->db->query($sql);
15✔
800
    }
801

802
    /**
803
     * @param array<string, array|string>|string $fields Field array or Field string
804
     *
805
     * @throws DatabaseException
806
     */
807
    public function modifyColumn(string $table, $fields): bool
808
    {
809
        // Work-around for literal column definitions
810
        if (is_string($fields)) {
5✔
811
            $fields = [$fields];
×
812
        }
813

814
        foreach (array_keys($fields) as $name) {
5✔
815
            $this->addField([$name => $fields[$name]]);
5✔
816
        }
817

818
        if ($this->fields === []) {
5✔
819
            throw new RuntimeException('Field information is required');
×
820
        }
821

822
        $sqls = $this->_alterTable('CHANGE', $this->db->DBPrefix . $table, $this->_processFields());
5✔
823
        $this->reset();
5✔
824

825
        if ($sqls === false) {
5✔
826
            if ($this->db->DBDebug) {
×
827
                throw new DatabaseException('This feature is not available for the database you are using.');
×
828
            }
829

830
            return false;
×
831
        }
832

833
        if (is_array($sqls)) {
5✔
834
            foreach ($sqls as $sql) {
4✔
835
                if ($this->db->query($sql) === false) {
4✔
836
                    return false;
×
837
                }
838
            }
839
        }
840

841
        return true;
5✔
842
    }
843

844
    /**
845
     * @param 'ADD'|'CHANGE'|'DROP' $alterType
846
     * @param array|string          $processedFields Processed column definitions
847
     *                                               or column names to DROP
848
     *
849
     * @return         false|list<string>|string|null                            SQL string
850
     * @phpstan-return ($alterType is 'DROP' ? string : list<string>|false|null)
851
     */
852
    protected function _alterTable(string $alterType, string $table, $processedFields)
853
    {
854
        $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . ' ';
18✔
855

856
        // DROP has everything it needs now.
857
        if ($alterType === 'DROP') {
18✔
858
            $columnNamesToDrop = $processedFields;
14✔
859

860
            if (is_string($columnNamesToDrop)) {
14✔
861
                $columnNamesToDrop = explode(',', $columnNamesToDrop);
13✔
862
            }
863

864
            $columnNamesToDrop = array_map(fn ($field) => 'DROP COLUMN ' . $this->db->escapeIdentifiers(trim($field)), $columnNamesToDrop);
14✔
865

866
            return $sql . implode(', ', $columnNamesToDrop);
14✔
867
        }
868

869
        $sql .= ($alterType === 'ADD') ? 'ADD ' : $alterType . ' COLUMN ';
15✔
870

871
        $sqls = [];
15✔
872

873
        foreach ($processedFields as $field) {
15✔
874
            $sqls[] = $sql . ($field['_literal'] !== false
15✔
875
                ? $field['_literal']
×
876
                : $this->_processColumn($field));
15✔
877
        }
878

879
        return $sqls;
15✔
880
    }
881

882
    /**
883
     * Returns $processedFields array from $this->fields data.
884
     */
885
    protected function _processFields(bool $createTable = false): array
886
    {
887
        $processedFields = [];
619✔
888

889
        foreach ($this->fields as $name => $attributes) {
619✔
890
            if (! is_array($attributes)) {
619✔
891
                $processedFields[] = ['_literal' => $attributes];
590✔
892

893
                continue;
590✔
894
            }
895

896
            $attributes = array_change_key_case($attributes, CASE_UPPER);
619✔
897

898
            if ($createTable === true && empty($attributes['TYPE'])) {
619✔
899
                continue;
×
900
            }
901

902
            if (isset($attributes['TYPE'])) {
619✔
903
                $this->_attributeType($attributes);
619✔
904
            }
905

906
            $field = [
619✔
907
                'name'           => $name,
619✔
908
                'new_name'       => $attributes['NAME'] ?? null,
619✔
909
                'type'           => $attributes['TYPE'] ?? null,
619✔
910
                'length'         => '',
619✔
911
                'unsigned'       => '',
619✔
912
                'null'           => '',
619✔
913
                'unique'         => '',
619✔
914
                'default'        => '',
619✔
915
                'auto_increment' => '',
619✔
916
                '_literal'       => false,
619✔
917
            ];
619✔
918

919
            if (isset($attributes['TYPE'])) {
619✔
920
                $this->_attributeUnsigned($attributes, $field);
619✔
921
            }
922

923
            if ($createTable === false) {
619✔
924
                if (isset($attributes['AFTER'])) {
20✔
925
                    $field['after'] = $attributes['AFTER'];
×
926
                } elseif (isset($attributes['FIRST'])) {
20✔
927
                    $field['first'] = (bool) $attributes['FIRST'];
×
928
                }
929
            }
930

931
            $this->_attributeDefault($attributes, $field);
619✔
932

933
            if (isset($attributes['NULL'])) {
619✔
934
                $nullString = ' ' . $this->null;
616✔
935

936
                if ($attributes['NULL'] === true) {
616✔
937
                    $field['null'] = empty($this->null) ? '' : $nullString;
607✔
938
                } elseif ($attributes['NULL'] === $nullString) {
607✔
939
                    $field['null'] = $nullString;
×
940
                } elseif ($attributes['NULL'] === '') {
607✔
941
                    $field['null'] = '';
×
942
                } else {
943
                    $field['null'] = ' NOT ' . $this->null;
607✔
944
                }
945
            } elseif ($createTable === true) {
619✔
946
                $field['null'] = ' NOT ' . $this->null;
619✔
947
            }
948

949
            $this->_attributeAutoIncrement($attributes, $field);
619✔
950
            $this->_attributeUnique($attributes, $field);
619✔
951

952
            if (isset($attributes['COMMENT'])) {
619✔
953
                $field['comment'] = $this->db->escape($attributes['COMMENT']);
×
954
            }
955

956
            if (isset($attributes['TYPE']) && ! empty($attributes['CONSTRAINT'])) {
619✔
957
                if (is_array($attributes['CONSTRAINT'])) {
619✔
958
                    $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']);
592✔
959
                    $attributes['CONSTRAINT'] = implode(',', $attributes['CONSTRAINT']);
592✔
960
                }
961

962
                $field['length'] = '(' . $attributes['CONSTRAINT'] . ')';
619✔
963
            }
964

965
            $processedFields[] = $field;
619✔
966
        }
967

968
        return $processedFields;
619✔
969
    }
970

971
    /**
972
     * Converts $processedField array to field definition string.
973
     */
974
    protected function _processColumn(array $processedField): string
975
    {
976
        return $this->db->escapeIdentifiers($processedField['name'])
1✔
977
            . ' ' . $processedField['type'] . $processedField['length']
1✔
978
            . $processedField['unsigned']
1✔
979
            . $processedField['default']
1✔
980
            . $processedField['null']
1✔
981
            . $processedField['auto_increment']
1✔
982
            . $processedField['unique'];
1✔
983
    }
984

985
    /**
986
     * Performs a data type mapping between different databases.
987
     */
988
    protected function _attributeType(array &$attributes)
989
    {
990
        // Usually overridden by drivers
991
    }
594✔
992

993
    /**
994
     * Depending on the unsigned property value:
995
     *
996
     *    - TRUE will always set $field['unsigned'] to 'UNSIGNED'
997
     *    - FALSE will always set $field['unsigned'] to ''
998
     *    - array(TYPE) will set $field['unsigned'] to 'UNSIGNED',
999
     *        if $attributes['TYPE'] is found in the array
1000
     *    - array(TYPE => UTYPE) will change $field['type'],
1001
     *        from TYPE to UTYPE in case of a match
1002
     */
1003
    protected function _attributeUnsigned(array &$attributes, array &$field)
1004
    {
1005
        if (empty($attributes['UNSIGNED']) || $attributes['UNSIGNED'] !== true) {
619✔
1006
            return;
619✔
1007
        }
1008

1009
        // Reset the attribute in order to avoid issues if we do type conversion
1010
        $attributes['UNSIGNED'] = false;
574✔
1011

1012
        if (is_array($this->unsigned)) {
574✔
1013
            foreach (array_keys($this->unsigned) as $key) {
5✔
1014
                if (is_int($key) && strcasecmp($attributes['TYPE'], $this->unsigned[$key]) === 0) {
5✔
1015
                    $field['unsigned'] = ' UNSIGNED';
1✔
1016

1017
                    return;
1✔
1018
                }
1019

1020
                if (is_string($key) && strcasecmp($attributes['TYPE'], $key) === 0) {
5✔
1021
                    $field['type'] = $key;
3✔
1022

1023
                    return;
3✔
1024
                }
1025
            }
1026

1027
            return;
5✔
1028
        }
1029

1030
        $field['unsigned'] = ($this->unsigned === true) ? ' UNSIGNED' : '';
574✔
1031
    }
1032

1033
    protected function _attributeDefault(array &$attributes, array &$field)
1034
    {
1035
        if ($this->default === false) {
619✔
1036
            return;
×
1037
        }
1038

1039
        if (array_key_exists('DEFAULT', $attributes)) {
619✔
1040
            if ($attributes['DEFAULT'] === null) {
4✔
1041
                $field['default'] = empty($this->null) ? '' : $this->default . $this->null;
×
1042

1043
                // Override the NULL attribute if that's our default
1044
                $attributes['NULL'] = true;
×
1045
                $field['null']      = empty($this->null) ? '' : ' ' . $this->null;
×
1046
            } elseif ($attributes['DEFAULT'] instanceof RawSql) {
4✔
1047
                $field['default'] = $this->default . $attributes['DEFAULT'];
1✔
1048
            } else {
1049
                $field['default'] = $this->default . $this->db->escape($attributes['DEFAULT']);
3✔
1050
            }
1051
        }
1052
    }
1053

1054
    protected function _attributeUnique(array &$attributes, array &$field)
1055
    {
1056
        if (! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) {
619✔
1057
            $field['unique'] = ' UNIQUE';
598✔
1058
        }
1059
    }
1060

1061
    protected function _attributeAutoIncrement(array &$attributes, array &$field)
1062
    {
1063
        if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
594✔
1064
            && stripos($field['type'], 'int') !== false
594✔
1065
        ) {
1066
            $field['auto_increment'] = ' AUTO_INCREMENT';
586✔
1067
        }
1068
    }
1069

1070
    /**
1071
     * Generates SQL to add primary key
1072
     *
1073
     * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
1074
     */
1075
    protected function _processPrimaryKeys(string $table, bool $asQuery = false): string
1076
    {
1077
        $sql = '';
619✔
1078

1079
        if (isset($this->primaryKeys['fields'])) {
619✔
1080
            for ($i = 0, $c = count($this->primaryKeys['fields']); $i < $c; $i++) {
607✔
1081
                if (! isset($this->fields[$this->primaryKeys['fields'][$i]])) {
607✔
1082
                    unset($this->primaryKeys['fields'][$i]);
×
1083
                }
1084
            }
1085
        }
1086

1087
        if (isset($this->primaryKeys['fields']) && $this->primaryKeys['fields'] !== []) {
619✔
1088
            if ($asQuery === true) {
607✔
1089
                $sql .= 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->DBPrefix . $table) . ' ADD ';
2✔
1090
            } else {
1091
                $sql .= ",\n\t";
607✔
1092
            }
1093
            $sql .= 'CONSTRAINT ' . $this->db->escapeIdentifiers(($this->primaryKeys['keyName'] === '' ?
607✔
1094
                'pk_' . $table :
607✔
1095
                $this->primaryKeys['keyName']))
607✔
1096
                    . ' PRIMARY KEY(' . implode(', ', $this->db->escapeIdentifiers($this->primaryKeys['fields'])) . ')';
607✔
1097
        }
1098

1099
        return $sql;
619✔
1100
    }
1101

1102
    /**
1103
     * Executes Sql to add indexes without createTable
1104
     */
1105
    public function processIndexes(string $table): bool
1106
    {
1107
        $sqls = [];
5✔
1108
        $fk   = $this->foreignKeys;
5✔
1109

1110
        if ($this->fields === []) {
5✔
1111
            $this->fields = array_flip(array_map(
5✔
1112
                static fn ($columnName) => $columnName->name,
5✔
1113
                $this->db->getFieldData($this->db->DBPrefix . $table)
5✔
1114
            ));
5✔
1115
        }
1116

1117
        $fields = $this->fields;
5✔
1118

1119
        if ($this->keys !== []) {
5✔
1120
            $sqls = $this->_processIndexes($this->db->DBPrefix . $table, true);
3✔
1121
        }
1122

1123
        if ($this->primaryKeys !== []) {
5✔
1124
            $sqls[] = $this->_processPrimaryKeys($table, true);
2✔
1125
        }
1126

1127
        $this->foreignKeys = $fk;
5✔
1128
        $this->fields      = $fields;
5✔
1129

1130
        if ($this->foreignKeys !== []) {
5✔
1131
            $sqls = array_merge($sqls, $this->_processForeignKeys($table, true));
2✔
1132
        }
1133

1134
        foreach ($sqls as $sql) {
5✔
1135
            if ($this->db->query($sql) === false) {
5✔
1136
                return false;
2✔
1137
            }
1138
        }
1139

1140
        $this->reset();
5✔
1141

1142
        return true;
5✔
1143
    }
1144

1145
    /**
1146
     * Generates SQL to add indexes
1147
     *
1148
     * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
1149
     */
1150
    protected function _processIndexes(string $table, bool $asQuery = false): array
1151
    {
1152
        $sqls = [];
588✔
1153

1154
        for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
588✔
1155
            for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
588✔
1156
                if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
588✔
1157
                    unset($this->keys[$i]['fields'][$i2]);
×
1158
                }
1159
            }
1160

1161
            if (count($this->keys[$i]['fields']) <= 0) {
588✔
1162
                continue;
×
1163
            }
1164

1165
            $keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
588✔
1166
                $table . '_' . implode('_', $this->keys[$i]['fields']) :
588✔
1167
                $this->keys[$i]['keyName']);
588✔
1168

1169
            if (in_array($i, $this->uniqueKeys, true)) {
588✔
1170
                if ($this->db->DBDriver === 'SQLite3') {
587✔
1171
                    $sqls[] = 'CREATE UNIQUE INDEX ' . $keyName
569✔
1172
                        . ' ON ' . $this->db->escapeIdentifiers($table)
569✔
1173
                        . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
569✔
1174
                } else {
1175
                    $sqls[] = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table)
580✔
1176
                        . ' ADD CONSTRAINT ' . $keyName
580✔
1177
                        . ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
580✔
1178
                }
1179

1180
                continue;
587✔
1181
            }
1182

1183
            $sqls[] = 'CREATE INDEX ' . $keyName
588✔
1184
                . ' ON ' . $this->db->escapeIdentifiers($table)
588✔
1185
                . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
588✔
1186
        }
1187

1188
        return $sqls;
588✔
1189
    }
1190

1191
    /**
1192
     * Generates SQL to add foreign keys
1193
     *
1194
     * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
1195
     */
1196
    protected function _processForeignKeys(string $table, bool $asQuery = false): array
1197
    {
1198
        $errorNames = [];
619✔
1199

1200
        foreach ($this->foreignKeys as $fkeyInfo) {
619✔
1201
            foreach ($fkeyInfo['field'] as $fieldName) {
14✔
1202
                if (! isset($this->fields[$fieldName])) {
14✔
1203
                    $errorNames[] = $fieldName;
2✔
1204
                }
1205
            }
1206
        }
1207

1208
        if ($errorNames !== []) {
619✔
1209
            $errorNames = [implode(', ', $errorNames)];
2✔
1210

1211
            throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
2✔
1212
        }
1213

1214
        $sqls = [''];
619✔
1215

1216
        foreach ($this->foreignKeys as $index => $fkey) {
619✔
1217
            if ($asQuery === false) {
12✔
1218
                $index = 0;
12✔
1219
            } else {
1220
                $sqls[$index] = '';
2✔
1221
            }
1222

1223
            $nameIndex = $fkey['fkName'] !== '' ?
12✔
1224
            $fkey['fkName'] :
5✔
1225
            $table . '_' . implode('_', $fkey['field']) . ($this->db->DBDriver === 'OCI8' ? '_fk' : '_foreign');
11✔
1226

1227
            $nameIndexFilled      = $this->db->escapeIdentifiers($nameIndex);
12✔
1228
            $foreignKeyFilled     = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
12✔
1229
            $referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
12✔
1230
            $referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
12✔
1231

1232
            if ($asQuery === true) {
12✔
1233
                $sqls[$index] .= 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->DBPrefix . $table) . ' ADD ';
2✔
1234
            } else {
1235
                $sqls[$index] .= ",\n\t";
12✔
1236
            }
1237

1238
            $formatSql = 'CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)';
12✔
1239
            $sqls[$index] .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
12✔
1240

1241
            if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $this->fkAllowActions, true)) {
12✔
1242
                $sqls[$index] .= ' ON DELETE ' . $fkey['onDelete'];
4✔
1243
            }
1244

1245
            if ($this->db->DBDriver !== 'OCI8' && $fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $this->fkAllowActions, true)) {
12✔
1246
                $sqls[$index] .= ' ON UPDATE ' . $fkey['onUpdate'];
4✔
1247
            }
1248
        }
1249

1250
        return $sqls;
619✔
1251
    }
1252

1253
    /**
1254
     * Resets table creation vars
1255
     */
1256
    public function reset()
1257
    {
1258
        $this->fields = $this->keys = $this->uniqueKeys = $this->primaryKeys = $this->foreignKeys = [];
620✔
1259
    }
1260
}
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