DBSchemaManager::transInitTable()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use Exception;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\ORM\DB;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\ORM\FieldType\DBPrimaryKey;
12
13
/**
14
 * Represents and handles all schema management for a database
15
 */
16
abstract class DBSchemaManager
17
{
18
19
    /**
20
     *
21
     * @config
22
     * Check tables when running /dev/build, and repair them if necessary.
23
     * In case of large databases or more fine-grained control on how to handle
24
     * data corruption in tables, you can disable this behaviour and handle it
25
     * outside of this class, e.g. through a nightly system task with extended logging capabilities.
26
     *
27
     * @var bool
28
     */
29
    private static $check_and_repair_on_build = true;
30
31
    /**
32
     * Check if tables should be renamed in a case-sensitive fashion.
33
     * Note: This should still work even on case-insensitive databases.
34
     *
35
     * @var bool
36
     */
37
    private static $fix_table_case_on_build = true;
38
39
    /**
40
     * Instance of the database controller this schema belongs to
41
     *
42
     * @var Database
43
     */
44
    protected $database = null;
45
46
    /**
47
     * If this is false, then information about database operations
48
     * will be displayed, eg creation of tables.
49
     *
50
     * @var boolean
51
     */
52
    protected $supressOutput = false;
53
54
    /**
55
     * Injector injection point for database controller
56
     *
57
     * @param Database $database
58
     */
59
    public function setDatabase(Database $database)
60
    {
61
        $this->database = $database;
62
    }
63
64
    /**
65
     * The table list, generated by the tableList() function.
66
     * Used by the requireTable() function.
67
     *
68
     * @var array
69
     */
70
    protected $tableList;
71
72
    /**
73
     * Keeps track whether we are currently updating the schema.
74
     *
75
     * @var boolean
76
     */
77
    protected $schemaIsUpdating = false;
78
79
    /**
80
     * Large array structure that represents a schema update transaction
81
     *
82
     * @var array
83
     */
84
    protected $schemaUpdateTransaction;
85
86
    /**
87
     * Enable supression of database messages.
88
     *
89
     * @param bool $quiet
90
     */
91
    public function quiet($quiet = true)
92
    {
93
        $this->supressOutput = $quiet;
94
    }
95
96
    /**
97
     * Execute the given SQL query.
98
     * This abstract function must be defined by subclasses as part of the actual implementation.
99
     * It should return a subclass of SS_Query as the result.
100
     *
101
     * @param string $sql The SQL query to execute
102
     * @param int $errorLevel The level of error reporting to enable for the query
103
     * @return Query
104
     */
105
    public function query($sql, $errorLevel = E_USER_ERROR)
106
    {
107
        return $this->database->query($sql, $errorLevel);
108
    }
109
110
111
    /**
112
     * Execute the given SQL parameterised query with the specified arguments
113
     *
114
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
115
     * @param array $parameters An ordered list of arguments.
116
     * @param int $errorLevel The level of error reporting to enable for the query
117
     * @return Query
118
     */
119
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
120
    {
121
        return $this->database->preparedQuery($sql, $parameters, $errorLevel);
122
    }
123
124
    /**
125
     * Initiates a schema update within a single callback
126
     *
127
     * @param callable $callback
128
     */
129
    public function schemaUpdate($callback)
130
    {
131
        // Begin schema update
132
        $this->schemaIsUpdating = true;
133
134
        // Update table list
135
        $this->tableList = array();
136
        $tables = $this->tableList();
137
        foreach ($tables as $table) {
138
            $this->tableList[strtolower($table)] = $table;
139
        }
140
141
        // Clear update list for client code to mess around with
142
        $this->schemaUpdateTransaction = array();
143
144
        /** @var Exception $error */
145
        $error = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $error is dead and can be removed.
Loading history...
146
        try {
147
            // Yield control to client code
148
            $callback();
149
150
            // If the client code has cancelled the update then abort
151
            if (!$this->isSchemaUpdating()) {
152
                return;
153
            }
154
155
            // End schema update
156
            foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
157
                $advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
158
                switch ($changes['command']) {
159
                    case 'create':
160
                        $this->createTable(
161
                            $tableName,
162
                            $changes['newFields'],
163
                            $changes['newIndexes'],
164
                            $changes['options'],
165
                            $advancedOptions
166
                        );
167
                        break;
168
169
                    case 'alter':
170
                        $this->alterTable(
171
                            $tableName,
172
                            $changes['newFields'],
173
                            $changes['newIndexes'],
174
                            $changes['alteredFields'],
175
                            $changes['alteredIndexes'],
176
                            $changes['alteredOptions'],
177
                            $advancedOptions
178
                        );
179
                        break;
180
                }
181
            }
182
        } finally {
183
            $this->schemaUpdateTransaction = null;
184
            $this->schemaIsUpdating = false;
185
        }
186
    }
187
188
    /**
189
     * Cancels the schema updates requested during (but not after) schemaUpdate() call.
190
     */
191
    public function cancelSchemaUpdate()
192
    {
193
        $this->schemaUpdateTransaction = null;
194
        $this->schemaIsUpdating = false;
195
    }
196
197
    /**
198
     * Returns true if we are during a schema update.
199
     *
200
     * @return boolean
201
     */
202
    public function isSchemaUpdating()
203
    {
204
        return $this->schemaIsUpdating;
205
    }
206
207
    /**
208
     * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
209
     *
210
     * @return boolean
211
     */
212
    public function doesSchemaNeedUpdating()
213
    {
214
        return (bool) $this->schemaUpdateTransaction;
215
    }
216
217
    // Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
218
219
    /**
220
     * Instruct the schema manager to record a table creation to later execute
221
     *
222
     * @param string $table Name of the table
223
     * @param array $options Create table options (ENGINE, etc.)
224
     * @param array $advanced_options Advanced table creation options
225
     */
226
    public function transCreateTable($table, $options = null, $advanced_options = null)
227
    {
228
        $this->schemaUpdateTransaction[$table] = array(
229
            'command' => 'create',
230
            'newFields' => array(),
231
            'newIndexes' => array(),
232
            'options' => $options,
233
            'advancedOptions' => $advanced_options
234
        );
235
    }
236
237
    /**
238
     * Instruct the schema manager to record a table alteration to later execute
239
     *
240
     * @param string $table Name of the table
241
     * @param array $options Create table options (ENGINE, etc.)
242
     * @param array $advanced_options Advanced table creation options
243
     */
244
    public function transAlterTable($table, $options, $advanced_options)
245
    {
246
        $this->transInitTable($table);
247
        $this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
248
        $this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
249
    }
250
251
    /**
252
     * Instruct the schema manager to record a field to be later created
253
     *
254
     * @param string $table Name of the table to hold this field
255
     * @param string $field Name of the field to create
256
     * @param string $schema Field specification as a string
257
     */
258
    public function transCreateField($table, $field, $schema)
259
    {
260
        $this->transInitTable($table);
261
        $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
262
    }
263
264
    /**
265
     * Instruct the schema manager to record an index to be later created
266
     *
267
     * @param string $table Name of the table to hold this index
268
     * @param string $index Name of the index to create
269
     * @param array $schema Already parsed index specification
270
     */
271
    public function transCreateIndex($table, $index, $schema)
272
    {
273
        $this->transInitTable($table);
274
        $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
275
    }
276
277
    /**
278
     * Instruct the schema manager to record a field to be later updated
279
     *
280
     * @param string $table Name of the table to hold this field
281
     * @param string $field Name of the field to update
282
     * @param string $schema Field specification as a string
283
     */
284
    public function transAlterField($table, $field, $schema)
285
    {
286
        $this->transInitTable($table);
287
        $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
288
    }
289
290
    /**
291
     * Instruct the schema manager to record an index to be later updated
292
     *
293
     * @param string $table Name of the table to hold this index
294
     * @param string $index Name of the index to update
295
     * @param array $schema Already parsed index specification
296
     */
297
    public function transAlterIndex($table, $index, $schema)
298
    {
299
        $this->transInitTable($table);
300
        $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
301
    }
302
303
    /**
304
     * Handler for the other transXXX methods - mark the given table as being altered
305
     * if it doesn't already exist
306
     *
307
     * @param string $table Name of the table to initialise
308
     */
309
    protected function transInitTable($table)
310
    {
311
        if (!isset($this->schemaUpdateTransaction[$table])) {
312
            $this->schemaUpdateTransaction[$table] = array(
313
                'command' => 'alter',
314
                'newFields' => array(),
315
                'newIndexes' => array(),
316
                'alteredFields' => array(),
317
                'alteredIndexes' => array(),
318
                'alteredOptions' => ''
319
            );
320
        }
321
    }
322
323
    /**
324
     * Generate the following table in the database, modifying whatever already exists
325
     * as necessary.
326
     *
327
     * @todo Change detection for CREATE TABLE $options other than "Engine"
328
     *
329
     * @param string $table The name of the table
330
     * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
331
     * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
332
     * The values of the array can be one of:
333
     *   - true: Create a single column index on the field named the same as the index.
334
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
335
     *     control over the index.
336
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
337
     * @param array $options Create table options (ENGINE, etc.)
338
     * @param array|bool $extensions List of extensions
339
     */
340
    public function requireTable(
341
        $table,
342
        $fieldSchema = null,
343
        $indexSchema = null,
344
        $hasAutoIncPK = true,
345
        $options = array(),
346
        $extensions = false
347
    ) {
348
        if (!isset($this->tableList[strtolower($table)])) {
349
            $this->transCreateTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type boolean; however, parameter $advanced_options of SilverStripe\ORM\Connect...ger::transCreateTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

349
            $this->transCreateTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
350
            $this->alterationMessage("Table $table: created", "created");
351
        } else {
352
            if (Config::inst()->get(static::class, 'fix_table_case_on_build')) {
353
                $this->fixTableCase($table);
354
            }
355
            if (Config::inst()->get(static::class, 'check_and_repair_on_build')) {
356
                $this->checkAndRepairTable($table);
357
            }
358
359
            // Check if options changed
360
            $tableOptionsChanged = false;
361
            // Check for DB constant on the schema class
362
            $dbIDName = sprintf('%s::ID', static::class);
363
            $dbID = defined($dbIDName) ? constant($dbIDName) : null;
364
            if ($dbID && isset($options[$dbID])) {
365
                if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
366
                    $alteredEngine = $alteredEngineMatches[1];
367
                    $tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->record();
368
                    $tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
369
                }
370
            }
371
372
            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type true; however, parameter $extensions of SilverStripe\ORM\Connect...e::supportsExtensions() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions(/** @scrutinizer ignore-type */ $extensions))) {
Loading history...
373
                $this->transAlterTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type boolean; however, parameter $advanced_options of SilverStripe\ORM\Connect...ager::transAlterTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

373
                $this->transAlterTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
374
            }
375
        }
376
377
        //DB ABSTRACTION: we need to convert this to a db-specific version:
378
        if (!isset($fieldSchema['ID'])) {
379
            $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
380
        }
381
382
        // Create custom fields
383
        if ($fieldSchema) {
384
            foreach ($fieldSchema as $fieldName => $fieldSpec) {
385
                //Is this an array field?
386
                $arrayValue = '';
387
                if (strpos($fieldSpec, '[') !== false) {
388
                    //If so, remove it and store that info separately
389
                    $pos = strpos($fieldSpec, '[');
390
                    $arrayValue = substr($fieldSpec, $pos);
391
                    $fieldSpec = substr($fieldSpec, 0, $pos);
392
                }
393
394
                /** @var DBField $fieldObj */
395
                $fieldObj = Injector::inst()->create($fieldSpec, $fieldName);
396
                $fieldObj->setArrayValue($arrayValue);
0 ignored issues
show
Bug introduced by
$arrayValue of type string is incompatible with the type array expected by parameter $value of SilverStripe\ORM\FieldTy...BField::setArrayValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

396
                $fieldObj->setArrayValue(/** @scrutinizer ignore-type */ $arrayValue);
Loading history...
397
398
                $fieldObj->setTable($table);
399
400
                if ($fieldObj instanceof DBPrimaryKey) {
401
                    /** @var DBPrimaryKey $fieldObj */
402
                    $fieldObj->setAutoIncrement($hasAutoIncPK);
403
                }
404
405
                $fieldObj->requireField();
406
            }
407
        }
408
409
        // Create custom indexes
410
        if ($indexSchema) {
411
            foreach ($indexSchema as $indexName => $indexSpec) {
412
                $this->requireIndex($table, $indexName, $indexSpec);
413
            }
414
        }
415
    }
416
417
    /**
418
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
419
     * @param string $table The table name.
420
     */
421
    public function dontRequireTable($table)
422
    {
423
        if (!isset($this->tableList[strtolower($table)])) {
424
            return;
425
        }
426
        $prefix = "_obsolete_{$table}";
427
        $suffix = '';
428
        $renameTo = $prefix . $suffix;
429
        while (isset($this->tableList[strtolower($renameTo)])) {
430
            $suffix = $suffix
431
                    ? ((int)$suffix + 1)
432
                    : 2;
433
            $renameTo = $prefix . $suffix;
434
        }
435
        $renameFrom = $this->tableList[strtolower($table)];
436
        $this->renameTable($renameFrom, $renameTo);
437
        $this->alterationMessage("Table $table: renamed to $renameTo", "obsolete");
438
    }
439
440
    /**
441
     * Generate the given index in the database, modifying whatever already exists as necessary.
442
     *
443
     * The keys of the array are the names of the index.
444
     * The values of the array can be one of:
445
     *  - true: Create a single column index on the field named the same as the index.
446
     *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
447
     *    control over the index.
448
     *
449
     * @param string $table The table name.
450
     * @param string $index The index name.
451
     * @param string|array|boolean $spec The specification of the index in any
452
     * loose format. See requireTable() for more information.
453
     */
454
    public function requireIndex($table, $index, $spec)
455
    {
456
        // Detect if adding to a new table
457
        $newTable = !isset($this->tableList[strtolower($table)]);
458
459
        // Force spec into standard array format
460
        $specString = $this->convertIndexSpec($spec);
461
462
        // Check existing index
463
        $oldSpecString = null;
464
        $indexKey = null;
465
        if (!$newTable) {
466
            $indexKey = $this->indexKey($table, $index, $spec);
467
            $indexList = $this->indexList($table);
468
            if (isset($indexList[$indexKey])) {
469
                // $oldSpec should be in standard array format
470
                $oldSpec = $indexList[$indexKey];
471
                $oldSpecString = $this->convertIndexSpec($oldSpec);
472
            }
473
        }
474
475
        // Initiate either generation or modification of index
476
        if ($newTable || !isset($indexList[$indexKey])) {
477
            // New index
478
            $this->transCreateIndex($table, $index, $spec);
479
            $this->alterationMessage("Index $table.$index: created as $specString", "created");
480
        } elseif ($oldSpecString != $specString) {
481
            // Updated index
482
            $this->transAlterIndex($table, $index, $spec);
483
            $this->alterationMessage(
484
                "Index $table.$index: changed to $specString <i class=\"build-info-before\">(from $oldSpecString)</i>",
485
                "changed"
486
            );
487
        }
488
    }
489
490
    /**
491
     * Splits a spec string safely, considering quoted columns, whitespace,
492
     * and cleaning brackets
493
     *
494
     * @param string $spec The input index specification string
495
     * @return array List of columns in the spec
496
     */
497
    protected function explodeColumnString($spec)
498
    {
499
        // Remove any leading/trailing brackets and outlying modifiers
500
        // E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
501
        $containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
502
503
        // Split potentially quoted modifiers
504
        // E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
505
        return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
506
    }
507
508
    /**
509
     * Builds a properly quoted column list from an array
510
     *
511
     * @param array $columns List of columns to implode
512
     * @return string A properly quoted list of column names
513
     */
514
    protected function implodeColumnList($columns)
515
    {
516
        if (empty($columns)) {
517
            return '';
518
        }
519
        return '"' . implode('","', $columns) . '"';
520
    }
521
522
    /**
523
     * Given an index specification in the form of a string ensure that each
524
     * column name is property quoted, stripping brackets and modifiers.
525
     * This index may also be in the form of a "CREATE INDEX..." sql fragment
526
     *
527
     * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
528
     * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
529
     */
530
    protected function quoteColumnSpecString($spec)
531
    {
532
        $bits = $this->explodeColumnString($spec);
533
        return $this->implodeColumnList($bits);
534
    }
535
536
    /**
537
     * Given an index spec determines the index type
538
     *
539
     * @param array|string $spec
540
     * @return string
541
     */
542
    protected function determineIndexType($spec)
543
    {
544
        // check array spec
545
        if (is_array($spec) && isset($spec['type'])) {
546
            return $spec['type'];
547
        } elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
548
            return strtolower($matchType['type']);
549
        } else {
550
            return 'index';
551
        }
552
    }
553
554
    /**
555
     * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
556
     * and turns it into a proper string.
557
     * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
558
     * arrays to be created. See {@link requireTable()} for details on the index format.
559
     *
560
     * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
561
     * @see parseIndexSpec() for approximate inverse
562
     *
563
     * @param string|array $indexSpec
564
     * @return string
565
     */
566
    protected function convertIndexSpec($indexSpec)
567
    {
568
        // Return already converted spec
569
        if (!is_array($indexSpec)
570
            || !array_key_exists('type', $indexSpec)
571
            || !array_key_exists('columns', $indexSpec)
572
            || !is_array($indexSpec['columns'])
573
            || array_key_exists('value', $indexSpec)
574
        ) {
575
            throw new \InvalidArgumentException(
576
                sprintf(
577
                    'argument to convertIndexSpec must be correct indexSpec, %s given',
578
                    var_export($indexSpec, true)
579
                )
580
            );
581
        }
582
583
        // Combine elements into standard string format
584
        return sprintf('%s (%s)', $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
585
    }
586
587
    /**
588
     * Returns true if the given table is exists in the current database
589
     *
590
     * @param string $tableName Name of table to check
591
     * @return boolean Flag indicating existence of table
592
     */
593
    abstract public function hasTable($tableName);
594
595
    /**
596
     * Return true if the table exists and already has a the field specified
597
     *
598
     * @param string $tableName - The table to check
599
     * @param string $fieldName - The field to check
600
     * @return bool - True if the table exists and the field exists on the table
601
     */
602
    public function hasField($tableName, $fieldName)
603
    {
604
        if (!$this->hasTable($tableName)) {
605
            return false;
606
        }
607
        $fields = $this->fieldList($tableName);
608
        return array_key_exists($fieldName, $fields);
609
    }
610
611
    /**
612
     * Generate the given field on the table, modifying whatever already exists as necessary.
613
     *
614
     * @param string $table The table name.
615
     * @param string $field The field name.
616
     * @param array|string $spec The field specification. If passed in array syntax, the specific database
617
     *  driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
618
     *  be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
619
     *  need to take care of database abstraction in your DBField subclass.
620
     */
621
    public function requireField($table, $field, $spec)
622
    {
623
        //TODO: this is starting to get extremely fragmented.
624
        //There are two different versions of $spec floating around, and their content changes depending
625
        //on how they are structured.  This needs to be tidied up.
626
        $fieldValue = null;
627
        $newTable = false;
628
629
        // backwards compatibility patch for pre 2.4 requireField() calls
630
        $spec_orig = $spec;
631
632
        if (!is_string($spec)) {
633
            $spec['parts']['name'] = $field;
634
            $spec_orig['parts']['name'] = $field;
635
            //Convert the $spec array into a database-specific string
636
            $spec = $this->{$spec['type']}($spec['parts'], true);
637
        }
638
639
        // Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
640
        // collations.
641
        // TODO: move this to the MySQLDatabase file, or drop it altogether?
642
        if (!$this->database->supportsCollations()) {
643
            $spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
644
        }
645
646
        if (!isset($this->tableList[strtolower($table)])) {
647
            $newTable = true;
648
        }
649
650
        if (is_array($spec)) {
651
            $specValue = $this->$spec_orig['type']($spec_orig['parts']);
652
        } else {
653
            $specValue = $spec;
654
        }
655
656
        // We need to get db-specific versions of the ID column:
657
        if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
658
            $specValue = $this->IdColumn(true);
659
        }
660
661
        if (!$newTable) {
662
            $fieldList = $this->fieldList($table);
663
            if (isset($fieldList[$field])) {
664
                if (is_array($fieldList[$field])) {
665
                    $fieldValue = $fieldList[$field]['data_type'];
666
                } else {
667
                    $fieldValue = $fieldList[$field];
668
                }
669
            }
670
        }
671
672
        // Get the version of the field as we would create it. This is used for comparison purposes to see if the
673
        // existing field is different to what we now want
674
        if (is_array($spec_orig)) {
675
            $spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
676
        }
677
678
        if ($newTable || $fieldValue == '') {
679
            $this->transCreateField($table, $field, $spec_orig);
680
            $this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
681
        } elseif ($fieldValue != $specValue) {
682
            // If enums/sets are being modified, then we need to fix existing data in the table.
683
            // Update any records where the enum is set to a legacy value to be set to the default.
684
            $enumValuesExpr = "/^(enum|set)\\s*\\(['\"](?<values>[^'\"]+)['\"]\\).*/i";
685
            if (preg_match($enumValuesExpr, $specValue, $specMatches)
686
                && preg_match($enumValuesExpr, $spec_orig, $oldMatches)
687
            ) {
688
                $new = preg_split("/'\\s*,\\s*'/", $specMatches['values']);
689
                $old = preg_split("/'\\s*,\\s*'/", $oldMatches['values']);
690
691
                $holder = array();
692
                foreach ($old as $check) {
693
                    if (!in_array($check, $new)) {
0 ignored issues
show
Bug introduced by
It seems like $new can also be of type false; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

693
                    if (!in_array($check, /** @scrutinizer ignore-type */ $new)) {
Loading history...
694
                        $holder[] = $check;
695
                    }
696
                }
697
698
                if (count($holder)) {
699
                    // Get default pre-escaped for SQL. We just use this directly, as we don't have a real way to
700
                    // de-encode SQL values
701
                        $default = explode('default ', $spec_orig);
702
                    $defaultSQL = isset($default[1]) ? $default[1] : 'NULL';
703
                    // Reset to default any value in that is in the old enum, but not the new one
704
                    $placeholders = DB::placeholders($holder);
705
                    $query = "UPDATE \"{$table}\" SET \"{$field}\" = {$defaultSQL} WHERE \"{$field}\" IN "
706
                        . "({$placeholders})";
707
                    $this->preparedQuery($query, $holder);
708
                        $amount = $this->database->affectedRows();
709
                    $this->alterationMessage(
710
                        "Changed $amount rows to default value of field $field (Value: $defaultSQL)"
711
                    );
712
                }
713
            }
714
            $this->transAlterField($table, $field, $spec_orig);
715
            $this->alterationMessage(
716
                "Field $table.$field: changed to $specValue <i class=\"build-info-before\">(from {$fieldValue})</i>",
717
                "changed"
718
            );
719
        }
720
    }
721
722
    /**
723
     * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
724
     *
725
     * @param string $table
726
     * @param string $fieldName
727
     */
728
    public function dontRequireField($table, $fieldName)
729
    {
730
        $fieldList = $this->fieldList($table);
731
        if (array_key_exists($fieldName, $fieldList)) {
732
            $suffix = '';
733
            while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
734
                $suffix = $suffix
735
                        ? ((int)$suffix + 1)
736
                        : 2;
737
            }
738
            $this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
739
            $this->alterationMessage(
740
                "Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
741
                "obsolete"
742
            );
743
        }
744
    }
745
746
    /**
747
     * Show a message about database alteration
748
     *
749
     * @param string $message to display
750
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
751
     */
752
    public function alterationMessage($message, $type = "")
753
    {
754
        if (!$this->supressOutput) {
755
            if (Director::is_cli()) {
756
                switch ($type) {
757
                    case "created":
758
                    case "changed":
759
                    case "repaired":
760
                        $sign = "+";
761
                        break;
762
                    case "obsolete":
763
                    case "deleted":
764
                        $sign = '-';
765
                        break;
766
                    case "notice":
767
                        $sign = '*';
768
                        break;
769
                    case "error":
770
                        $sign = "!";
771
                        break;
772
                    default:
773
                        $sign = " ";
774
                }
775
                $message = strip_tags($message);
776
                echo "  $sign $message\n";
777
            } else {
778
                switch ($type) {
779
                    case "created":
780
                        $class = "success";
781
                        break;
782
                    case "obsolete":
783
                        $class = "error";
784
                        break;
785
                    case "notice":
786
                        $class = "warning";
787
                        break;
788
                    case "error":
789
                        $class = "error";
790
                        break;
791
                    case "deleted":
792
                        $class = "error";
793
                        break;
794
                    case "changed":
795
                        $class = "info";
796
                        break;
797
                    case "repaired":
798
                        $class = "info";
799
                        break;
800
                    default:
801
                        $class = "";
802
                }
803
                echo "<li class=\"$class\">$message</li>";
804
            }
805
        }
806
    }
807
808
    /**
809
     * This returns the data type for the id column which is the primary key for each table
810
     *
811
     * @param boolean $asDbValue
812
     * @param boolean $hasAutoIncPK
813
     * @return string
814
     */
815
    abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
816
817
    /**
818
     * Checks a table's integrity and repairs it if necessary.
819
     *
820
     * @param string $tableName The name of the table.
821
     * @return boolean Return true if the table has integrity after the method is complete.
822
     */
823
    abstract public function checkAndRepairTable($tableName);
824
825
826
    /**
827
     * Ensure the given table has the correct case
828
     *
829
     * @param string $tableName Name of table in desired case
830
     */
831
    public function fixTableCase($tableName)
832
    {
833
        // Check if table exists
834
        $tables = $this->tableList();
835
        if (!array_key_exists(strtolower($tableName), $tables)) {
836
            return;
837
        }
838
839
        // Check if case differs
840
        $currentName = $tables[strtolower($tableName)];
841
        if ($currentName === $tableName) {
842
            return;
843
        }
844
845
        $this->alterationMessage(
846
            "Table $tableName: renamed from $currentName",
847
            "repaired"
848
        );
849
850
        // Rename via temp table to avoid case-sensitivity issues
851
        $tempTable = "__TEMP__{$tableName}";
852
        $this->renameTable($currentName, $tempTable);
853
        $this->renameTable($tempTable, $tableName);
854
    }
855
856
    /**
857
     * Returns the values of the given enum field
858
     *
859
     * @param string $tableName Name of table to check
860
     * @param string $fieldName name of enum field to check
861
     * @return array List of enum values
862
     */
863
    abstract public function enumValuesForField($tableName, $fieldName);
864
865
866
    /*
867
     * This is a lookup table for data types.
868
     * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
869
     * So this is a DB-specific list of equivilents.
870
     *
871
     * @param string $type
872
     * @return string
873
     */
874
    abstract public function dbDataType($type);
875
876
    /**
877
     * Retrieves the list of all databases the user has access to
878
     *
879
     * @return array List of database names
880
     */
881
    abstract public function databaseList();
882
883
    /**
884
     * Determine if the database with the specified name exists
885
     *
886
     * @param string $name Name of the database to check for
887
     * @return boolean Flag indicating whether this database exists
888
     */
889
    abstract public function databaseExists($name);
890
891
    /**
892
     * Create a database with the specified name
893
     *
894
     * @param string $name Name of the database to create
895
     * @return boolean True if successful
896
     */
897
    abstract public function createDatabase($name);
898
899
    /**
900
     * Drops a database with the specified name
901
     *
902
     * @param string $name Name of the database to drop
903
     */
904
    abstract public function dropDatabase($name);
905
906
    /**
907
     * Alter an index on a table.
908
     *
909
     * @param string $tableName The name of the table.
910
     * @param string $indexName The name of the index.
911
     * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
912
     * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
913
     */
914
    abstract public function alterIndex($tableName, $indexName, $indexSpec);
915
916
    /**
917
     * Determines the key that should be used to identify this index
918
     * when retrieved from DBSchemaManager->indexList.
919
     * In some connectors this is the database-visible name, in others the
920
     * usercode-visible name.
921
     *
922
     * @param string $table
923
     * @param string $index
924
     * @param array $spec
925
     * @return string Key for this index
926
     */
927
    abstract protected function indexKey($table, $index, $spec);
928
929
    /**
930
     * Return the list of indexes in a table.
931
     *
932
     * @param string $table The table name.
933
     * @return array[array] List of current indexes in the table, each in standard
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[array] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
934
     * array form. The key for this array should be predictable using the indexKey
935
     * method
936
     */
937
    abstract public function indexList($table);
938
939
    /**
940
     * Returns a list of all tables in the database.
941
     * Keys are table names in lower case, values are table names in case that
942
     * database expects.
943
     *
944
     * @return array
945
     */
946
    abstract public function tableList();
947
948
    /**
949
     * Create a new table.
950
     *
951
     * @param string $table The name of the table
952
     * @param array $fields A map of field names to field types
953
     * @param array $indexes A map of indexes
954
     * @param array $options An map of additional options.  The available keys are as follows:
955
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
956
     *   - 'temporary' - If true, then a temporary table will be created
957
     * @param array $advancedOptions Advanced creation options
958
     * @return string The table name generated.  This may be different from the table name, for example with temporary
959
     * tables.
960
     */
961
    abstract public function createTable(
962
        $table,
963
        $fields = null,
964
        $indexes = null,
965
        $options = null,
966
        $advancedOptions = null
967
    );
968
969
    /**
970
     * Alter a table's schema.
971
     *
972
     * @param string $table The name of the table to alter
973
     * @param array $newFields New fields, a map of field name => field schema
974
     * @param array $newIndexes New indexes, a map of index name => index type
975
     * @param array $alteredFields Updated fields, a map of field name => field schema
976
     * @param array $alteredIndexes Updated indexes, a map of index name => index type
977
     * @param array $alteredOptions
978
     * @param array $advancedOptions
979
     */
980
    abstract public function alterTable(
981
        $table,
982
        $newFields = null,
983
        $newIndexes = null,
984
        $alteredFields = null,
985
        $alteredIndexes = null,
986
        $alteredOptions = null,
987
        $advancedOptions = null
988
    );
989
990
    /**
991
     * Rename a table.
992
     *
993
     * @param string $oldTableName The old table name.
994
     * @param string $newTableName The new table name.
995
     */
996
    abstract public function renameTable($oldTableName, $newTableName);
997
998
    /**
999
     * Create a new field on a table.
1000
     *
1001
     * @param string $table Name of the table.
1002
     * @param string $field Name of the field to add.
1003
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
1004
     */
1005
    abstract public function createField($table, $field, $spec);
1006
1007
    /**
1008
     * Change the database column name of the given field.
1009
     *
1010
     * @param string $tableName The name of the tbale the field is in.
1011
     * @param string $oldName The name of the field to change.
1012
     * @param string $newName The new name of the field
1013
     */
1014
    abstract public function renameField($tableName, $oldName, $newName);
1015
1016
    /**
1017
     * Get a list of all the fields for the given table.
1018
     * Returns a map of field name => field spec.
1019
     *
1020
     * @param string $table The table name.
1021
     * @return array
1022
     */
1023
    abstract public function fieldList($table);
1024
1025
    /**
1026
     *
1027
     * This allows the cached values for a table's field list to be erased.
1028
     * If $tablename is empty, then the whole cache is erased.
1029
     *
1030
     * @param string $tableName
1031
     * @return boolean
1032
     */
1033
    public function clearCachedFieldlist($tableName = null)
1034
    {
1035
        return true;
1036
    }
1037
1038
1039
    /**
1040
     * Returns data type for 'boolean' column
1041
     *
1042
     * @param array $values Contains a tokenised list of info about this data type
1043
     * @return string
1044
     */
1045
    abstract public function boolean($values);
1046
1047
    /**
1048
     * Returns data type for 'date' column
1049
     *
1050
     * @param array $values Contains a tokenised list of info about this data type
1051
     * @return string
1052
     */
1053
    abstract public function date($values);
1054
1055
    /**
1056
     * Returns data type for 'decimal' column
1057
     *
1058
     * @param array $values Contains a tokenised list of info about this data type
1059
     * @return string
1060
     */
1061
    abstract public function decimal($values);
1062
1063
    /**
1064
     * Returns data type for 'set' column
1065
     *
1066
     * @param array $values Contains a tokenised list of info about this data type
1067
     * @return string
1068
     */
1069
    abstract public function enum($values);
1070
1071
    /**
1072
     * Returns data type for 'set' column
1073
     *
1074
     * @param array $values Contains a tokenised list of info about this data type
1075
     * @return string
1076
     */
1077
    abstract public function set($values);
1078
1079
    /**
1080
     * Returns data type for 'float' column
1081
     *
1082
     * @param array $values Contains a tokenised list of info about this data type
1083
     * @return string
1084
     */
1085
    abstract public function float($values);
1086
1087
    /**
1088
     * Returns data type for 'int' column
1089
     *
1090
     * @param array $values Contains a tokenised list of info about this data type
1091
     * @return string
1092
     */
1093
    abstract public function int($values);
1094
1095
    /**
1096
     * Returns data type for 'datetime' column
1097
     *
1098
     * @param array $values Contains a tokenised list of info about this data type
1099
     * @return string
1100
     */
1101
    abstract public function datetime($values);
1102
1103
    /**
1104
     * Returns data type for 'text' column
1105
     *
1106
     * @param array $values Contains a tokenised list of info about this data type
1107
     * @return string
1108
     */
1109
    abstract public function text($values);
1110
1111
    /**
1112
     * Returns data type for 'time' column
1113
     *
1114
     * @param array $values Contains a tokenised list of info about this data type
1115
     * @return string
1116
     */
1117
    abstract public function time($values);
1118
1119
    /**
1120
     * Returns data type for 'varchar' column
1121
     *
1122
     * @param array $values Contains a tokenised list of info about this data type
1123
     * @return string
1124
     */
1125
    abstract public function varchar($values);
1126
1127
    /*
1128
     * Returns data type for 'year' column
1129
     *
1130
     * @param array $values Contains a tokenised list of info about this data type
1131
     * @return string
1132
     */
1133
    abstract public function year($values);
1134
}
1135