ci4

Форк
0
/
Forge.php 
1260 строк · 36.2 Кб
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;
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;
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) {
216
            if ($this->databaseExists($dbName)) {
217
                return true;
218
            }
219

220
            $ifNotExists = false;
221
        }
222

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

228
            return false; // @codeCoverageIgnore
229
        }
230

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

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

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

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

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) {
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;
279
    }
280

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

293
            return false;
294
        }
295

296
        if (! $this->db->query(
297
            sprintf($this->dropDatabaseStr, $this->db->escapeIdentifier($dbName))
298
        )) {
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'])) {
307
            $key = array_search(
308
                strtolower($dbName),
309
                array_map(strtolower(...), $this->db->dataCache['db_names']),
310
                true
311
            );
312
            if ($key !== false) {
313
                unset($this->db->dataCache['db_names'][$key]);
314
            }
315
        }
316

317
        return true;
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) {
330
            $this->primaryKeys = ['fields' => (array) $key, 'keyName' => $keyName];
331
        } else {
332
            $this->keys[] = ['fields' => (array) $key, 'keyName' => $keyName];
333

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

339
        return $this;
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);
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);
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)) {
376
            if ($fields === 'id') {
377
                $this->addField([
378
                    'id' => [
379
                        'type'           => 'INT',
380
                        'constraint'     => 9,
381
                        'auto_increment' => true,
382
                    ],
383
                ]);
384
                $this->addKey('id', true);
385
            } else {
386
                if (! str_contains($fields, ' ')) {
387
                    throw new InvalidArgumentException('Field information is required for that operation.');
388
                }
389

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

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

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

402
                    continue;
403
                }
404

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

411
        return $this;
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;
431
        $tableField = (array) $tableField;
432

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

442
        return $this;
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);
453
        $table   = $this->db->escapeIdentifiers($this->db->DBPrefix . $table);
454

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

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

471
        if ($sql === '') {
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);
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);
488

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

493
        return $this->db->query($sql)->getResultArray() !== [];
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 '';
502
    }
503

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

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

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

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

536
            return false;
537
        }
538

539
        return $this->db->query($sql);
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 === '') {
552
            throw new InvalidArgumentException('A table name is required for that operation.');
553
        }
554

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

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

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

565
            return true;
566
        }
567

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

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

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

583
        $this->reset();
584

585
        return $result;
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);
598

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

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

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

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

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

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

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

635
        return $sql;
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 === '') {
646
            if ($this->db->DBDebug) {
647
                throw new DatabaseException('A table name is required for that operation.');
648
            }
649

650
            return false;
651
        }
652

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

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

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

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

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

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

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

679
        return $query;
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';
690

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

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

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

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

720
            return false;
721
        }
722

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

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

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

741
        return $result;
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)) {
753
            $fields = [$fields];
754
        }
755

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

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

763
        if ($sqls === false) {
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) {
772
            if ($this->db->query($sql) === false) {
773
                return false;
774
            }
775
        }
776

777
        return true;
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);
790

791
        if ($sql === false) {
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);
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)) {
811
            $fields = [$fields];
812
        }
813

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

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

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

825
        if ($sqls === false) {
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)) {
834
            foreach ($sqls as $sql) {
835
                if ($this->db->query($sql) === false) {
836
                    return false;
837
                }
838
            }
839
        }
840

841
        return true;
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) . ' ';
855

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

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

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

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

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

871
        $sqls = [];
872

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

879
        return $sqls;
880
    }
881

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

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

893
                continue;
894
            }
895

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

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

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

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

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

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

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

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

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

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

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

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

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

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

968
        return $processedFields;
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'])
977
            . ' ' . $processedField['type'] . $processedField['length']
978
            . $processedField['unsigned']
979
            . $processedField['default']
980
            . $processedField['null']
981
            . $processedField['auto_increment']
982
            . $processedField['unique'];
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
    }
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) {
1006
            return;
1007
        }
1008

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

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

1017
                    return;
1018
                }
1019

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

1023
                    return;
1024
                }
1025
            }
1026

1027
            return;
1028
        }
1029

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

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

1039
        if (array_key_exists('DEFAULT', $attributes)) {
1040
            if ($attributes['DEFAULT'] === null) {
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) {
1047
                $field['default'] = $this->default . $attributes['DEFAULT'];
1048
            } else {
1049
                $field['default'] = $this->default . $this->db->escape($attributes['DEFAULT']);
1050
            }
1051
        }
1052
    }
1053

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

1061
    protected function _attributeAutoIncrement(array &$attributes, array &$field)
1062
    {
1063
        if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
1064
            && stripos($field['type'], 'int') !== false
1065
        ) {
1066
            $field['auto_increment'] = ' AUTO_INCREMENT';
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 = '';
1078

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

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

1099
        return $sql;
1100
    }
1101

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

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

1117
        $fields = $this->fields;
1118

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

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

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

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

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

1140
        $this->reset();
1141

1142
        return true;
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 = [];
1153

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

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

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

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

1180
                continue;
1181
            }
1182

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

1188
        return $sqls;
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 = [];
1199

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

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

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

1214
        $sqls = [''];
1215

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

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

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

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

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

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

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

1250
        return $sqls;
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 = [];
1259
    }
1260
}
1261

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.