Completed
Pull Request — master (#6930)
by Daniel
09:37
created

DBSchemaManager::convertIndexSpec()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 4
nc 2
nop 1
dl 0
loc 10
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\ORM\FieldType\DBPrimaryKey;
9
use SilverStripe\ORM\FieldType\DBField;
10
use Exception;
11
12
/**
13
 * Represents and handles all schema management for a database
14
 */
15
abstract class DBSchemaManager
16
{
17
18
    /**
19
     *
20
     * @config
21
     * Check tables when running /dev/build, and repair them if necessary.
22
     * In case of large databases or more fine-grained control on how to handle
23
     * data corruption in tables, you can disable this behaviour and handle it
24
     * outside of this class, e.g. through a nightly system task with extended logging capabilities.
25
     *
26
     * @var bool
27
     */
28
    private static $check_and_repair_on_build = true;
29
30
    /**
31
     * Check if tables should be renamed in a case-sensitive fashion.
32
     * Note: This should still work even on case-insensitive databases.
33
     *
34
     * @var bool
35
     */
36
    private static $fix_table_case_on_build = true;
37
38
    /**
39
     * Instance of the database controller this schema belongs to
40
     *
41
     * @var Database
42
     */
43
    protected $database = null;
44
45
    /**
46
     * If this is false, then information about database operations
47
     * will be displayed, eg creation of tables.
48
     *
49
     * @var boolean
50
     */
51
    protected $supressOutput = false;
52
53
    /**
54
     * Injector injection point for database controller
55
     *
56
     * @param Database $database
57
     */
58
    public function setDatabase(Database $database)
59
    {
60
        $this->database = $database;
61
    }
62
63
    /**
64
     * The table list, generated by the tableList() function.
65
     * Used by the requireTable() function.
66
     *
67
     * @var array
68
     */
69
    protected $tableList;
70
71
    /**
72
     * Keeps track whether we are currently updating the schema.
73
     *
74
     * @var boolean
75
     */
76
    protected $schemaIsUpdating = false;
77
78
    /**
79
     * Large array structure that represents a schema update transaction
80
     *
81
     * @var array
82
     */
83
    protected $schemaUpdateTransaction;
84
85
    /**
86
     * Enable supression of database messages.
87
     */
88
    public function quiet()
89
    {
90
        $this->supressOutput = true;
91
    }
92
93
    /**
94
     * Execute the given SQL query.
95
     * This abstract function must be defined by subclasses as part of the actual implementation.
96
     * It should return a subclass of SS_Query as the result.
97
     *
98
     * @param string $sql The SQL query to execute
99
     * @param int $errorLevel The level of error reporting to enable for the query
100
     * @return Query
101
     */
102
    public function query($sql, $errorLevel = E_USER_ERROR)
103
    {
104
        return $this->database->query($sql, $errorLevel);
105
    }
106
107
108
    /**
109
     * Execute the given SQL parameterised query with the specified arguments
110
     *
111
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
112
     * @param array $parameters An ordered list of arguments.
113
     * @param int $errorLevel The level of error reporting to enable for the query
114
     * @return Query
115
     */
116
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
117
    {
118
        return $this->database->preparedQuery($sql, $parameters, $errorLevel);
119
    }
120
121
    /**
122
     * Initiates a schema update within a single callback
123
     *
124
     * @param callable $callback
125
     */
126
    public function schemaUpdate($callback)
127
    {
128
        // Begin schema update
129
        $this->schemaIsUpdating = true;
130
131
        // Update table list
132
        $this->tableList = array();
133
        $tables = $this->tableList();
134
        foreach ($tables as $table) {
135
            $this->tableList[strtolower($table)] = $table;
136
        }
137
138
        // Clear update list for client code to mess around with
139
        $this->schemaUpdateTransaction = array();
140
141
        /** @var Exception $error */
142
        $error = null;
143
        try {
144
            // Yield control to client code
145
            $callback();
146
147
            // If the client code has cancelled the update then abort
148
            if (!$this->isSchemaUpdating()) {
149
                return;
150
            }
151
152
            // End schema update
153
            foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
154
                $advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
155
                switch ($changes['command']) {
156
                    case 'create':
157
                        $this->createTable(
158
                            $tableName,
159
                            $changes['newFields'],
160
                            $changes['newIndexes'],
161
                            $changes['options'],
162
                            $advancedOptions
163
                        );
164
                        break;
165
166
                    case 'alter':
167
                        $this->alterTable(
168
                            $tableName,
169
                            $changes['newFields'],
170
                            $changes['newIndexes'],
171
                            $changes['alteredFields'],
172
                            $changes['alteredIndexes'],
173
                            $changes['alteredOptions'],
174
                            $advancedOptions
175
                        );
176
                        break;
177
                }
178
            }
179
        } finally {
180
            $this->schemaUpdateTransaction = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $schemaUpdateTransaction.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
181
            $this->schemaIsUpdating = false;
182
        }
183
    }
184
185
    /**
186
     * Cancels the schema updates requested during (but not after) schemaUpdate() call.
187
     */
188
    public function cancelSchemaUpdate()
189
    {
190
        $this->schemaUpdateTransaction = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $schemaUpdateTransaction.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
191
        $this->schemaIsUpdating = false;
192
    }
193
194
    /**
195
     * Returns true if we are during a schema update.
196
     *
197
     * @return boolean
198
     */
199
    function isSchemaUpdating()
200
    {
201
        return $this->schemaIsUpdating;
202
    }
203
204
    /**
205
     * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
206
     *
207
     * @return boolean
208
     */
209
    public function doesSchemaNeedUpdating()
210
    {
211
        return (bool) $this->schemaUpdateTransaction;
212
    }
213
214
    // Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
215
216
    /**
217
     * Instruct the schema manager to record a table creation to later execute
218
     *
219
     * @param string $table Name of the table
220
     * @param array $options Create table options (ENGINE, etc.)
221
     * @param array $advanced_options Advanced table creation options
222
     */
223
    public function transCreateTable($table, $options = null, $advanced_options = null)
224
    {
225
        $this->schemaUpdateTransaction[$table] = array(
226
            'command' => 'create',
227
            'newFields' => array(),
228
            'newIndexes' => array(),
229
            'options' => $options,
230
            'advancedOptions' => $advanced_options
231
        );
232
    }
233
234
    /**
235
     * Instruct the schema manager to record a table alteration to later execute
236
     *
237
     * @param string $table Name of the table
238
     * @param array $options Create table options (ENGINE, etc.)
239
     * @param array $advanced_options Advanced table creation options
240
     */
241
    public function transAlterTable($table, $options, $advanced_options)
242
    {
243
        $this->transInitTable($table);
244
        $this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
245
        $this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
246
    }
247
248
    /**
249
     * Instruct the schema manager to record a field to be later created
250
     *
251
     * @param string $table Name of the table to hold this field
252
     * @param string $field Name of the field to create
253
     * @param string $schema Field specification as a string
254
     */
255
    public function transCreateField($table, $field, $schema)
256
    {
257
        $this->transInitTable($table);
258
        $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
259
    }
260
261
    /**
262
     * Instruct the schema manager to record an index to be later created
263
     *
264
     * @param string $table Name of the table to hold this index
265
     * @param string $index Name of the index to create
266
     * @param array $schema Already parsed index specification
267
     */
268
    public function transCreateIndex($table, $index, $schema)
269
    {
270
        $this->transInitTable($table);
271
        $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
272
    }
273
274
    /**
275
     * Instruct the schema manager to record a field to be later updated
276
     *
277
     * @param string $table Name of the table to hold this field
278
     * @param string $field Name of the field to update
279
     * @param string $schema Field specification as a string
280
     */
281
    public function transAlterField($table, $field, $schema)
282
    {
283
        $this->transInitTable($table);
284
        $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
285
    }
286
287
    /**
288
     * Instruct the schema manager to record an index to be later updated
289
     *
290
     * @param string $table Name of the table to hold this index
291
     * @param string $index Name of the index to update
292
     * @param array $schema Already parsed index specification
293
     */
294
    public function transAlterIndex($table, $index, $schema)
295
    {
296
        $this->transInitTable($table);
297
        $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
298
    }
299
300
    /**
301
     * Handler for the other transXXX methods - mark the given table as being altered
302
     * if it doesn't already exist
303
     *
304
     * @param string $table Name of the table to initialise
305
     */
306
    protected function transInitTable($table)
307
    {
308
        if (!isset($this->schemaUpdateTransaction[$table])) {
309
            $this->schemaUpdateTransaction[$table] = array(
310
                'command' => 'alter',
311
                'newFields' => array(),
312
                'newIndexes' => array(),
313
                'alteredFields' => array(),
314
                'alteredIndexes' => array(),
315
                'alteredOptions' => ''
316
            );
317
        }
318
    }
319
320
    /**
321
     * Generate the following table in the database, modifying whatever already exists
322
     * as necessary.
323
     *
324
     * @todo Change detection for CREATE TABLE $options other than "Engine"
325
     *
326
     * @param string $table The name of the table
327
     * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
328
     * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
329
     * The values of the array can be one of:
330
     *   - true: Create a single column index on the field named the same as the index.
331
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
332
     *     control over the index.
333
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
334
     * @param array $options Create table options (ENGINE, etc.)
335
     * @param array|bool $extensions List of extensions
336
     */
337
    public function requireTable(
338
        $table,
339
        $fieldSchema = null,
340
        $indexSchema = null,
341
        $hasAutoIncPK = true,
342
        $options = array(),
343
        $extensions = false
344
    ) {
345
        if (!isset($this->tableList[strtolower($table)])) {
346
            $this->transCreateTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 343 can also be of type boolean; however, SilverStripe\ORM\Connect...ger::transCreateTable() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

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

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
370
                $this->transAlterTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 343 can also be of type boolean; however, SilverStripe\ORM\Connect...ager::transAlterTable() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
371
            }
372
        }
373
374
        //DB ABSTRACTION: we need to convert this to a db-specific version:
375
        if (!isset($fieldSchema['ID'])) {
376
            $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
377
        }
378
379
        // Create custom fields
380
        if ($fieldSchema) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldSchema of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
381
            foreach ($fieldSchema as $fieldName => $fieldSpec) {
382
                //Is this an array field?
383
                $arrayValue = '';
384
                if (strpos($fieldSpec, '[') !== false) {
385
                    //If so, remove it and store that info separately
386
                    $pos = strpos($fieldSpec, '[');
387
                    $arrayValue = substr($fieldSpec, $pos);
388
                    $fieldSpec = substr($fieldSpec, 0, $pos);
389
                }
390
391
                /** @var DBField $fieldObj */
392
                $fieldObj = Injector::inst()->create($fieldSpec, $fieldName);
393
                $fieldObj->setArrayValue($arrayValue);
394
395
                $fieldObj->setTable($table);
396
397
                if ($fieldObj instanceof DBPrimaryKey) {
398
                    /** @var DBPrimaryKey $fieldObj */
399
                    $fieldObj->setAutoIncrement($hasAutoIncPK);
400
                }
401
402
                $fieldObj->requireField();
403
            }
404
        }
405
406
        // Create custom indexes
407
        if ($indexSchema) {
408
            foreach ($indexSchema as $indexName => $indexSpec) {
409
                $this->requireIndex($table, $indexName, $indexSpec);
410
            }
411
        }
412
    }
413
414
    /**
415
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
416
     * @param string $table The table name.
417
     */
418
    public function dontRequireTable($table)
419
    {
420
        if (isset($this->tableList[strtolower($table)])) {
421
            $suffix = '';
422
            while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
423
                $suffix = $suffix
424
                        ? ((int)$suffix + 1)
425
                        : 2;
426
            }
427
            $this->renameTable($table, "_obsolete_{$table}$suffix");
428
            $this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
429
        }
430
    }
431
432
    /**
433
     * Generate the given index in the database, modifying whatever already exists as necessary.
434
     *
435
     * The keys of the array are the names of the index.
436
     * The values of the array can be one of:
437
     *  - true: Create a single column index on the field named the same as the index.
438
     *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
439
     *    control over the index.
440
     *
441
     * @param string $table The table name.
442
     * @param string $index The index name.
443
     * @param string|array|boolean $spec The specification of the index in any
444
     * loose format. See requireTable() for more information.
445
     */
446
    public function requireIndex($table, $index, $spec)
447
    {
448
        // Detect if adding to a new table
449
        $newTable = !isset($this->tableList[strtolower($table)]);
450
451
        // Force spec into standard array format
452
        $specString = $this->convertIndexSpec($spec);
453
454
        // Check existing index
455
        $oldSpecString = null;
456
        $indexKey = null;
457
        if (!$newTable) {
458
            $indexKey = $this->indexKey($table, $index, $spec);
459
            $indexList = $this->indexList($table);
460
            if (isset($indexList[$indexKey])) {
461
                // $oldSpec should be in standard array format
462
                $oldSpec = $indexList[$indexKey];
463
                $oldSpecString = $this->convertIndexSpec($oldSpec);
464
            }
465
        }
466
467
        // Initiate either generation or modification of index
468
        if ($newTable || !isset($indexList[$indexKey])) {
469
            // New index
470
            $this->transCreateIndex($table, $index, $spec);
471
            $this->alterationMessage("Index $table.$index: created as $specString", "created");
472
        } elseif ($oldSpecString != $specString) {
473
            // Updated index
474
            $this->transAlterIndex($table, $index, $spec);
475
            $this->alterationMessage(
476
                "Index $table.$index: changed to $specString <i style=\"color: #AAA\">(from $oldSpecString)</i>",
477
                "changed"
478
            );
479
        }
480
    }
481
482
    /**
483
     * Splits a spec string safely, considering quoted columns, whitespace,
484
     * and cleaning brackets
485
     *
486
     * @param string $spec The input index specification string
487
     * @return array List of columns in the spec
488
     */
489
    protected function explodeColumnString($spec)
490
    {
491
        // Remove any leading/trailing brackets and outlying modifiers
492
        // E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
493
        $containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
494
495
        // Split potentially quoted modifiers
496
        // E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
497
        return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
498
    }
499
500
    /**
501
     * Builds a properly quoted column list from an array
502
     *
503
     * @param array $columns List of columns to implode
504
     * @return string A properly quoted list of column names
505
     */
506
    protected function implodeColumnList($columns)
507
    {
508
        if (empty($columns)) {
509
            return '';
510
        }
511
        return '"' . implode('","', $columns) . '"';
512
    }
513
514
    /**
515
     * Given an index specification in the form of a string ensure that each
516
     * column name is property quoted, stripping brackets and modifiers.
517
     * This index may also be in the form of a "CREATE INDEX..." sql fragment
518
     *
519
     * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
520
     * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
521
     */
522
    protected function quoteColumnSpecString($spec)
523
    {
524
        $bits = $this->explodeColumnString($spec);
525
        return $this->implodeColumnList($bits);
526
    }
527
528
    /**
529
     * Given an index spec determines the index type
530
     *
531
     * @param array|string $spec
532
     * @return string
533
     */
534
    protected function determineIndexType($spec)
535
    {
536
        // check array spec
537
        if (is_array($spec) && isset($spec['type'])) {
538
            return $spec['type'];
539
        } elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
540
            return strtolower($matchType['type']);
541
        } else {
542
            return 'index';
543
        }
544
    }
545
546
    /**
547
     * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
548
     * and turns it into a proper string.
549
     * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
550
     * arrays to be created. See {@link requireTable()} for details on the index format.
551
     *
552
     * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
553
     * @see parseIndexSpec() for approximate inverse
554
     *
555
     * @param string|array $indexSpec
556
     * @return string
557
     */
558
    protected function convertIndexSpec($indexSpec)
559
    {
560
        // Return already converted spec
561
        if (!is_array($indexSpec) || !array_key_exists('type', $indexSpec) || !array_key_exists('columns', $indexSpec) || !is_array($indexSpec['columns'])) {
562
            throw new \InvalidArgumentException(sprintf('argument to convertIndexSpec must be correct indexSpec, %s given', var_export($indexSpec, true)));
563
        }
564
565
        // Combine elements into standard string format
566
        return sprintf('%s (%s)', $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
567
    }
568
569
    /**
570
     * Returns true if the given table is exists in the current database
571
     *
572
     * @param string $tableName Name of table to check
573
     * @return boolean Flag indicating existence of table
574
     */
575
    abstract public function hasTable($tableName);
576
577
    /**
578
     * Return true if the table exists and already has a the field specified
579
     *
580
     * @param string $tableName - The table to check
581
     * @param string $fieldName - The field to check
582
     * @return bool - True if the table exists and the field exists on the table
583
     */
584
    public function hasField($tableName, $fieldName)
585
    {
586
        if (!$this->hasTable($tableName)) {
587
            return false;
588
        }
589
        $fields = $this->fieldList($tableName);
590
        return array_key_exists($fieldName, $fields);
591
    }
592
593
    /**
594
     * Generate the given field on the table, modifying whatever already exists as necessary.
595
     *
596
     * @param string $table The table name.
597
     * @param string $field The field name.
598
     * @param array|string $spec The field specification. If passed in array syntax, the specific database
599
     *  driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
600
     *  be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
601
     *  need to take care of database abstraction in your DBField subclass.
602
     */
603
    public function requireField($table, $field, $spec)
604
    {
605
        //TODO: this is starting to get extremely fragmented.
606
        //There are two different versions of $spec floating around, and their content changes depending
607
        //on how they are structured.  This needs to be tidied up.
608
        $fieldValue = null;
609
        $newTable = false;
610
611
        // backwards compatibility patch for pre 2.4 requireField() calls
612
        $spec_orig = $spec;
613
614
        if (!is_string($spec)) {
615
            $spec['parts']['name'] = $field;
616
            $spec_orig['parts']['name'] = $field;
617
            //Convert the $spec array into a database-specific string
618
            $spec = $this->{$spec['type']}($spec['parts'], true);
619
        }
620
621
        // Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
622
        // collations.
623
        // TODO: move this to the MySQLDatabase file, or drop it altogether?
624
        if (!$this->database->supportsCollations()) {
625
            $spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
626
        }
627
628
        if (!isset($this->tableList[strtolower($table)])) {
629
            $newTable = true;
630
        }
631
632
        if (is_array($spec)) {
633
            $specValue = $this->$spec_orig['type']($spec_orig['parts']);
634
        } else {
635
            $specValue = $spec;
636
        }
637
638
        // We need to get db-specific versions of the ID column:
639
        if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
640
            $specValue = $this->IdColumn(true);
641
        }
642
643
        if (!$newTable) {
644
            $fieldList = $this->fieldList($table);
645
            if (isset($fieldList[$field])) {
646
                if (is_array($fieldList[$field])) {
647
                    $fieldValue = $fieldList[$field]['data_type'];
648
                } else {
649
                    $fieldValue = $fieldList[$field];
650
                }
651
            }
652
        }
653
654
        // Get the version of the field as we would create it. This is used for comparison purposes to see if the
655
        // existing field is different to what we now want
656
        if (is_array($spec_orig)) {
657
            $spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
658
        }
659
660
        if ($newTable || $fieldValue == '') {
661
            $this->transCreateField($table, $field, $spec_orig);
662
            $this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
663
        } elseif ($fieldValue != $specValue) {
664
            // If enums/sets are being modified, then we need to fix existing data in the table.
665
            // Update any records where the enum is set to a legacy value to be set to the default.
666
            foreach (array('enum', 'set') as $enumtype) {
667
                if (preg_match("/^$enumtype/i", $specValue)) {
668
                    $newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
669
                    $new = preg_split("/'\\s*,\\s*'/", $newStr);
670
671
                    $oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
672
                    $old = preg_split("/'\\s*,\\s*'/", $oldStr);
673
674
                    $holder = array();
675
                    foreach ($old as $check) {
676
                        if (!in_array($check, $new)) {
677
                            $holder[] = $check;
678
                        }
679
                    }
680
                    if (count($holder)) {
681
                        $default = explode('default ', $spec_orig);
682
                        $default = $default[1];
683
                        $query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
684
                        for ($i = 0; $i + 1 < count($holder); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
685
                            $query .= "'{$holder[$i]}', ";
686
                        }
687
                        $query .= "'{$holder[$i]}')";
688
                        $this->query($query);
689
                        $amount = $this->database->affectedRows();
690
                        $this->alterationMessage("Changed $amount rows to default value of field $field"
691
                                . " (Value: $default)");
692
                    }
693
                }
694
            }
695
            $this->transAlterField($table, $field, $spec_orig);
696
            $this->alterationMessage(
697
                "Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
698
                "changed"
699
            );
700
        }
701
    }
702
703
    /**
704
     * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
705
     *
706
     * @param string $table
707
     * @param string $fieldName
708
     */
709
    public function dontRequireField($table, $fieldName)
710
    {
711
        $fieldList = $this->fieldList($table);
712
        if (array_key_exists($fieldName, $fieldList)) {
713
            $suffix = '';
714
            while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
715
                $suffix = $suffix
716
                        ? ((int)$suffix + 1)
717
                        : 2;
718
            }
719
            $this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
720
            $this->alterationMessage(
721
                "Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
722
                "obsolete"
723
            );
724
        }
725
    }
726
727
    /**
728
     * Show a message about database alteration
729
     *
730
     * @param string $message to display
731
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
732
     */
733
    public function alterationMessage($message, $type = "")
734
    {
735
        if (!$this->supressOutput) {
736
            if (Director::is_cli()) {
737
                switch ($type) {
738
                    case "created":
739
                    case "changed":
740
                    case "repaired":
741
                        $sign = "+";
742
                        break;
743
                    case "obsolete":
744
                    case "deleted":
745
                        $sign = '-';
746
                        break;
747
                    case "notice":
748
                        $sign = '*';
749
                        break;
750
                    case "error":
751
                        $sign = "!";
752
                        break;
753
                    default:
754
                        $sign = " ";
755
                }
756
                $message = strip_tags($message);
757
                echo "  $sign $message\n";
758
            } else {
759
                switch ($type) {
760
                    case "created":
761
                        $color = "green";
762
                        break;
763
                    case "obsolete":
764
                        $color = "red";
765
                        break;
766
                    case "notice":
767
                        $color = "orange";
768
                        break;
769
                    case "error":
770
                        $color = "red";
771
                        break;
772
                    case "deleted":
773
                        $color = "red";
774
                        break;
775
                    case "changed":
776
                        $color = "blue";
777
                        break;
778
                    case "repaired":
779
                        $color = "blue";
780
                        break;
781
                    default:
782
                        $color = "";
783
                }
784
                echo "<li style=\"color: $color\">$message</li>";
785
            }
786
        }
787
    }
788
789
    /**
790
     * This returns the data type for the id column which is the primary key for each table
791
     *
792
     * @param boolean $asDbValue
793
     * @param boolean $hasAutoIncPK
794
     * @return string
795
     */
796
    abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
797
798
    /**
799
     * Checks a table's integrity and repairs it if necessary.
800
     *
801
     * @param string $tableName The name of the table.
802
     * @return boolean Return true if the table has integrity after the method is complete.
803
     */
804
    abstract public function checkAndRepairTable($tableName);
805
806
807
    /**
808
     * Ensure the given table has the correct case
809
     *
810
     * @param string $tableName Name of table in desired case
811
     */
812
    public function fixTableCase($tableName)
813
    {
814
        // Check if table exists
815
        $tables = $this->tableList();
816
        if (!array_key_exists(strtolower($tableName), $tables)) {
817
            return;
818
        }
819
820
        // Check if case differs
821
        $currentName = $tables[strtolower($tableName)];
822
        if ($currentName === $tableName) {
823
            return;
824
        }
825
826
        $this->alterationMessage(
827
            "Table $tableName: renamed from $currentName",
828
            "repaired"
829
        );
830
831
        // Rename via temp table to avoid case-sensitivity issues
832
        $tempTable = "__TEMP__{$tableName}";
833
        $this->renameTable($currentName, $tempTable);
834
        $this->renameTable($tempTable, $tableName);
835
    }
836
837
    /**
838
     * Returns the values of the given enum field
839
     *
840
     * @param string $tableName Name of table to check
841
     * @param string $fieldName name of enum field to check
842
     * @return array List of enum values
843
     */
844
    abstract public function enumValuesForField($tableName, $fieldName);
845
846
847
    /*
848
	 * This is a lookup table for data types.
849
	 * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
850
	 * So this is a DB-specific list of equivilents.
851
	 *
852
	 * @param string $type
853
	 * @return string
854
	 */
855
    abstract public function dbDataType($type);
856
857
    /**
858
     * Retrieves the list of all databases the user has access to
859
     *
860
     * @return array List of database names
861
     */
862
    abstract public function databaseList();
863
864
    /**
865
     * Determine if the database with the specified name exists
866
     *
867
     * @param string $name Name of the database to check for
868
     * @return boolean Flag indicating whether this database exists
869
     */
870
    abstract public function databaseExists($name);
871
872
    /**
873
     * Create a database with the specified name
874
     *
875
     * @param string $name Name of the database to create
876
     * @return boolean True if successful
877
     */
878
    abstract public function createDatabase($name);
879
880
    /**
881
     * Drops a database with the specified name
882
     *
883
     * @param string $name Name of the database to drop
884
     */
885
    abstract public function dropDatabase($name);
886
887
    /**
888
     * Alter an index on a table.
889
     *
890
     * @param string $tableName The name of the table.
891
     * @param string $indexName The name of the index.
892
     * @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
893
     *                          for more details.
894
     * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
895
     */
896
    abstract public function alterIndex($tableName, $indexName, $indexSpec);
897
898
    /**
899
     * Determines the key that should be used to identify this index
900
     * when retrieved from DBSchemaManager->indexList.
901
     * In some connectors this is the database-visible name, in others the
902
     * usercode-visible name.
903
     *
904
     * @param string $table
905
     * @param string $index
906
     * @param array $spec
907
     * @return string Key for this index
908
     */
909
    abstract protected function indexKey($table, $index, $spec);
910
911
    /**
912
     * Return the list of indexes in a table.
913
     *
914
     * @param string $table The table name.
915
     * @return array[array] List of current indexes in the table, each in standard
0 ignored issues
show
Documentation introduced by
The doc-type array[array] could not be parsed: Expected "]" at position 2, but found "array". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
916
     * array form. The key for this array should be predictable using the indexKey
917
     * method
918
     */
919
    abstract public function indexList($table);
920
921
    /**
922
     * Returns a list of all tables in the database.
923
     * Keys are table names in lower case, values are table names in case that
924
     * database expects.
925
     *
926
     * @return array
927
     */
928
    abstract public function tableList();
929
930
    /**
931
     * Create a new table.
932
     *
933
     * @param string $table The name of the table
934
     * @param array $fields A map of field names to field types
935
     * @param array $indexes A map of indexes
936
     * @param array $options An map of additional options.  The available keys are as follows:
937
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
938
     *   - 'temporary' - If true, then a temporary table will be created
939
     * @param array $advancedOptions Advanced creation options
940
     * @return string The table name generated.  This may be different from the table name, for example with temporary
941
     * tables.
942
     */
943
    abstract public function createTable(
944
        $table,
945
        $fields = null,
946
        $indexes = null,
947
        $options = null,
948
        $advancedOptions = null
949
    );
950
951
    /**
952
     * Alter a table's schema.
953
     *
954
     * @param string $table The name of the table to alter
955
     * @param array $newFields New fields, a map of field name => field schema
956
     * @param array $newIndexes New indexes, a map of index name => index type
957
     * @param array $alteredFields Updated fields, a map of field name => field schema
958
     * @param array $alteredIndexes Updated indexes, a map of index name => index type
959
     * @param array $alteredOptions
960
     * @param array $advancedOptions
961
     */
962
    abstract public function alterTable(
963
        $table,
964
        $newFields = null,
965
        $newIndexes = null,
966
        $alteredFields = null,
967
        $alteredIndexes = null,
968
        $alteredOptions = null,
969
        $advancedOptions = null
970
    );
971
972
    /**
973
     * Rename a table.
974
     *
975
     * @param string $oldTableName The old table name.
976
     * @param string $newTableName The new table name.
977
     */
978
    abstract public function renameTable($oldTableName, $newTableName);
979
980
    /**
981
     * Create a new field on a table.
982
     *
983
     * @param string $table Name of the table.
984
     * @param string $field Name of the field to add.
985
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
986
     */
987
    abstract public function createField($table, $field, $spec);
988
989
    /**
990
     * Change the database column name of the given field.
991
     *
992
     * @param string $tableName The name of the tbale the field is in.
993
     * @param string $oldName The name of the field to change.
994
     * @param string $newName The new name of the field
995
     */
996
    abstract public function renameField($tableName, $oldName, $newName);
997
998
    /**
999
     * Get a list of all the fields for the given table.
1000
     * Returns a map of field name => field spec.
1001
     *
1002
     * @param string $table The table name.
1003
     * @return array
1004
     */
1005
    abstract public function fieldList($table);
1006
1007
    /**
1008
     *
1009
     * This allows the cached values for a table's field list to be erased.
1010
     * If $tablename is empty, then the whole cache is erased.
1011
     *
1012
     * @param string $tableName
1013
     * @return boolean
1014
     */
1015
    public function clearCachedFieldlist($tableName = null)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1016
    {
1017
        return true;
1018
    }
1019
1020
1021
    /**
1022
     * Returns data type for 'boolean' column
1023
     *
1024
     * @param array $values Contains a tokenised list of info about this data type
1025
     * @return string
1026
     */
1027
    abstract public function boolean($values);
1028
1029
    /**
1030
     * Returns data type for 'date' column
1031
     *
1032
     * @param array $values Contains a tokenised list of info about this data type
1033
     * @return string
1034
     */
1035
    abstract public function date($values);
1036
1037
    /**
1038
     * Returns data type for 'decimal' column
1039
     *
1040
     * @param array $values Contains a tokenised list of info about this data type
1041
     * @return string
1042
     */
1043
    abstract public function decimal($values);
1044
1045
    /**
1046
     * Returns data type for 'set' column
1047
     *
1048
     * @param array $values Contains a tokenised list of info about this data type
1049
     * @return string
1050
     */
1051
    abstract public function enum($values);
1052
1053
    /**
1054
     * Returns data type for 'set' column
1055
     *
1056
     * @param array $values Contains a tokenised list of info about this data type
1057
     * @return string
1058
     */
1059
    abstract public function set($values);
1060
1061
    /**
1062
     * Returns data type for 'float' column
1063
     *
1064
     * @param array $values Contains a tokenised list of info about this data type
1065
     * @return string
1066
     */
1067
    abstract public function float($values);
1068
1069
    /**
1070
     * Returns data type for 'int' column
1071
     *
1072
     * @param array $values Contains a tokenised list of info about this data type
1073
     * @return string
1074
     */
1075
    abstract public function int($values);
1076
1077
    /**
1078
     * Returns data type for 'datetime' column
1079
     *
1080
     * @param array $values Contains a tokenised list of info about this data type
1081
     * @return string
1082
     */
1083
    abstract public function datetime($values);
1084
1085
    /**
1086
     * Returns data type for 'text' column
1087
     *
1088
     * @param array $values Contains a tokenised list of info about this data type
1089
     * @return string
1090
     */
1091
    abstract public function text($values);
1092
1093
    /**
1094
     * Returns data type for 'time' column
1095
     *
1096
     * @param array $values Contains a tokenised list of info about this data type
1097
     * @return string
1098
     */
1099
    abstract public function time($values);
1100
1101
    /**
1102
     * Returns data type for 'varchar' column
1103
     *
1104
     * @param array $values Contains a tokenised list of info about this data type
1105
     * @return string
1106
     */
1107
    abstract public function varchar($values);
1108
1109
    /*
1110
	 * Returns data type for 'year' column
1111
	 *
1112
	 * @param array $values Contains a tokenised list of info about this data type
1113
	 * @return string
1114
	 */
1115
    abstract public function year($values);
1116
}
1117