Completed
Pull Request — master (#6203)
by Damian
25:45 queued 16:48
created

DBSchemaManager::schemaUpdate()   C

Complexity

Conditions 7
Paths 28

Size

Total Lines 45
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
c 0
b 0
f 0
nc 28
nop 1
dl 0
loc 45
rs 6.7272
1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Object;
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
	 * @config
20
	 * Check tables when running /dev/build, and repair them if necessary.
21
	 * In case of large databases or more fine-grained control on how to handle
22
	 * data corruption in tables, you can disable this behaviour and handle it
23
	 * outside of this class, e.g. through a nightly system task with extended logging capabilities.
24
	 *
25
	 * @var bool
26
	 */
27
	private static $check_and_repair_on_build = true;
28
29
	/**
30
	 * Check if tables should be renamed in a case-sensitive fashion.
31
	 * Note: This should still work even on case-insensitive databases.
32
	 *
33
	 * @var bool
34
	 */
35
	private static $fix_table_case_on_build = true;
36
37
	/**
38
	 * Instance of the database controller this schema belongs to
39
	 *
40
	 * @var Database
41
	 */
42
	protected $database = null;
43
44
	/**
45
	 * If this is false, then information about database operations
46
	 * will be displayed, eg creation of tables.
47
	 *
48
	 * @var boolean
49
	 */
50
	protected $supressOutput = false;
51
52
	/**
53
	 * Injector injection point for database controller
54
	 *
55
	 * @param Database $database
56
	 */
57
	public function setDatabase(Database $database) {
58
		$this->database = $database;
59
	}
60
61
	/**
62
	 * The table list, generated by the tableList() function.
63
	 * Used by the requireTable() function.
64
	 *
65
	 * @var array
66
	 */
67
	protected $tableList;
68
69
	/**
70
	 * Keeps track whether we are currently updating the schema.
71
	 *
72
	 * @var boolean
73
	 */
74
	protected $schemaIsUpdating = false;
75
76
	/**
77
	 * Large array structure that represents a schema update transaction
78
	 *
79
	 * @var array
80
	 */
81
	protected $schemaUpdateTransaction;
82
83
	/**
84
	 * Enable supression of database messages.
85
	 */
86
	public function quiet() {
87
		$this->supressOutput = true;
88
	}
89
90
	/**
91
	 * Execute the given SQL query.
92
	 * This abstract function must be defined by subclasses as part of the actual implementation.
93
	 * It should return a subclass of SS_Query as the result.
94
	 *
95
	 * @param string $sql The SQL query to execute
96
	 * @param int $errorLevel The level of error reporting to enable for the query
97
	 * @return Query
98
	 */
99
	public function query($sql, $errorLevel = E_USER_ERROR) {
100
		return $this->database->query($sql, $errorLevel);
101
	}
102
103
104
	/**
105
	 * Execute the given SQL parameterised query with the specified arguments
106
	 *
107
	 * @param string $sql The SQL query to execute. The ? character will denote parameters.
108
	 * @param array $parameters An ordered list of arguments.
109
	 * @param int $errorLevel The level of error reporting to enable for the query
110
	 * @return Query
111
	 */
112
	public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
113
		return $this->database->preparedQuery($sql, $parameters, $errorLevel);
114
	}
115
116
	/**
117
	 * Initiates a schema update within a single callback
118
	 *
119
	 * @param callable $callback
120
	 */
121
	public function schemaUpdate($callback) {
122
		// Begin schema update
123
		$this->schemaIsUpdating = true;
124
125
		// Update table list
126
		$this->tableList = array();
127
		$tables = $this->tableList();
128
		foreach ($tables as $table) {
129
			$this->tableList[strtolower($table)] = $table;
130
		}
131
132
		// Clear update list for client code to mess around with
133
		$this->schemaUpdateTransaction = array();
134
135
		/** @var Exception $error */
136
		$error = null;
137
		try {
138
139
			// Yield control to client code
140
			$callback();
141
142
			// If the client code has cancelled the update then abort
143
			if(!$this->isSchemaUpdating()) return;
144
145
			// End schema update
146
			foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
147
				$advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
148
				switch ($changes['command']) {
149
					case 'create':
150
						$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'],
151
										$changes['options'], $advancedOptions);
152
						break;
153
154
					case 'alter':
155
						$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
156
										$changes['alteredFields'], $changes['alteredIndexes'],
157
										$changes['alteredOptions'], $advancedOptions);
158
						break;
159
				}
160
			}
161
		} finally {
162
			$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...
163
			$this->schemaIsUpdating = false;
164
		}
165
	}
166
167
	/**
168
	 * Cancels the schema updates requested during (but not after) schemaUpdate() call.
169
	 */
170
	public function cancelSchemaUpdate() {
171
		$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...
172
		$this->schemaIsUpdating = false;
173
	}
174
175
	/**
176
	 * Returns true if we are during a schema update.
177
	 *
178
	 * @return boolean
179
	 */
180
	function isSchemaUpdating() {
181
		return $this->schemaIsUpdating;
182
	}
183
184
	/**
185
	 * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
186
	 *
187
	 * @return boolean
188
	 */
189
	public function doesSchemaNeedUpdating() {
190
		return (bool) $this->schemaUpdateTransaction;
191
	}
192
193
	// Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
194
195
	/**
196
	 * Instruct the schema manager to record a table creation to later execute
197
	 *
198
	 * @param string $table Name of the table
199
	 * @param array $options Create table options (ENGINE, etc.)
200
	 * @param array $advanced_options Advanced table creation options
201
	 */
202
	public function transCreateTable($table, $options = null, $advanced_options = null) {
203
		$this->schemaUpdateTransaction[$table] = array(
204
			'command' => 'create',
205
			'newFields' => array(),
206
			'newIndexes' => array(),
207
			'options' => $options,
208
			'advancedOptions' => $advanced_options
209
		);
210
	}
211
212
	/**
213
	 * Instruct the schema manager to record a table alteration to later execute
214
	 *
215
	 * @param string $table Name of the table
216
	 * @param array $options Create table options (ENGINE, etc.)
217
	 * @param array $advanced_options Advanced table creation options
218
	 */
219
	public function transAlterTable($table, $options, $advanced_options) {
220
		$this->transInitTable($table);
221
		$this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
222
		$this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
223
	}
224
225
	/**
226
	 * Instruct the schema manager to record a field to be later created
227
	 *
228
	 * @param string $table Name of the table to hold this field
229
	 * @param string $field Name of the field to create
230
	 * @param string $schema Field specification as a string
231
	 */
232
	public function transCreateField($table, $field, $schema) {
233
		$this->transInitTable($table);
234
		$this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
235
	}
236
237
	/**
238
	 * Instruct the schema manager to record an index to be later created
239
	 *
240
	 * @param string $table Name of the table to hold this index
241
	 * @param string $index Name of the index to create
242
	 * @param array $schema Already parsed index specification
243
	 */
244
	public function transCreateIndex($table, $index, $schema) {
245
		$this->transInitTable($table);
246
		$this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
247
	}
248
249
	/**
250
	 * Instruct the schema manager to record a field to be later updated
251
	 *
252
	 * @param string $table Name of the table to hold this field
253
	 * @param string $field Name of the field to update
254
	 * @param string $schema Field specification as a string
255
	 */
256
	public function transAlterField($table, $field, $schema) {
257
		$this->transInitTable($table);
258
		$this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
259
	}
260
261
	/**
262
	 * Instruct the schema manager to record an index to be later updated
263
	 *
264
	 * @param string $table Name of the table to hold this index
265
	 * @param string $index Name of the index to update
266
	 * @param array $schema Already parsed index specification
267
	 */
268
	public function transAlterIndex($table, $index, $schema) {
269
		$this->transInitTable($table);
270
		$this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
271
	}
272
273
	/**
274
	 * Handler for the other transXXX methods - mark the given table as being altered
275
	 * if it doesn't already exist
276
	 *
277
	 * @param string $table Name of the table to initialise
278
	 */
279
	protected function transInitTable($table) {
280
		if (!isset($this->schemaUpdateTransaction[$table])) {
281
			$this->schemaUpdateTransaction[$table] = array(
282
				'command' => 'alter',
283
				'newFields' => array(),
284
				'newIndexes' => array(),
285
				'alteredFields' => array(),
286
				'alteredIndexes' => array(),
287
				'alteredOptions' => ''
288
			);
289
		}
290
	}
291
292
	/**
293
	 * Generate the following table in the database, modifying whatever already exists
294
	 * as necessary.
295
	 *
296
	 * @todo Change detection for CREATE TABLE $options other than "Engine"
297
	 *
298
	 * @param string $table The name of the table
299
	 * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
300
	 * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
301
	 * The values of the array can be one of:
302
	 *   - true: Create a single column index on the field named the same as the index.
303
	 *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
304
	 *     control over the index.
305
	 * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
306
	 * @param array $options Create table options (ENGINE, etc.)
307
	 * @param array|bool $extensions List of extensions
308
	 */
309
	public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
310
		$options = array(), $extensions = false
311
	) {
312
		if (!isset($this->tableList[strtolower($table)])) {
313
			$this->transCreateTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 310 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...
314
			$this->alterationMessage("Table $table: created", "created");
315
		} else {
316
			if (Config::inst()->get(static::class, 'fix_table_case_on_build')) {
317
				$this->fixTableCase($table);
318
			}
319
			if (Config::inst()->get(static::class, 'check_and_repair_on_build')) {
320
				$this->checkAndRepairTable($table);
321
			}
322
323
			// Check if options changed
324
			$tableOptionsChanged = false;
325
			// Check for DB constant on the schema class
326
			$dbIDName = sprintf('%s::ID', get_class($this));
327
			$dbID = defined($dbIDName) ? constant($dbIDName) : null;
328
			if ($dbID && isset($options[$dbID])) {
329
				if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
330
					$alteredEngine = $alteredEngineMatches[1];
331
					$tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
332
					$tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
333
				}
334
			}
335
336
			if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
0 ignored issues
show
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 310 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...
337
				$this->transAlterTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 310 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...
338
			}
339
		}
340
341
		//DB ABSTRACTION: we need to convert this to a db-specific version:
342
		if(!isset($fieldSchema['ID'])) {
343
			$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
344
		}
345
346
		// Create custom fields
347
		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...
348
			foreach ($fieldSchema as $fieldName => $fieldSpec) {
349
350
				//Is this an array field?
351
				$arrayValue = '';
352
				if (strpos($fieldSpec, '[') !== false) {
353
					//If so, remove it and store that info separately
354
					$pos = strpos($fieldSpec, '[');
355
					$arrayValue = substr($fieldSpec, $pos);
356
					$fieldSpec = substr($fieldSpec, 0, $pos);
357
				}
358
359
				/** @var DBField $fieldObj */
360
				$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
361
				$fieldObj->setArrayValue($arrayValue);
362
363
				$fieldObj->setTable($table);
364
365
				if($fieldObj instanceof DBPrimaryKey) {
366
					/** @var DBPrimaryKey $fieldObj */
367
					$fieldObj->setAutoIncrement($hasAutoIncPK);
368
				}
369
370
				$fieldObj->requireField();
371
			}
372
		}
373
374
		// Create custom indexes
375
		if ($indexSchema) {
376
			foreach ($indexSchema as $indexName => $indexDetails) {
377
				$this->requireIndex($table, $indexName, $indexDetails);
378
			}
379
		}
380
	}
381
382
	/**
383
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
384
	 * @param string $table The table name.
385
	 */
386
	public function dontRequireTable($table) {
387
		if (isset($this->tableList[strtolower($table)])) {
388
			$suffix = '';
389
			while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
390
				$suffix = $suffix
391
						? ((int)$suffix + 1)
392
						: 2;
393
			}
394
			$this->renameTable($table, "_obsolete_{$table}$suffix");
395
			$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
396
		}
397
	}
398
399
	/**
400
	 * Generate the given index in the database, modifying whatever already exists as necessary.
401
	 *
402
	 * The keys of the array are the names of the index.
403
	 * The values of the array can be one of:
404
	 *  - true: Create a single column index on the field named the same as the index.
405
	 *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
406
	 *    control over the index.
407
	 *
408
	 * @param string $table The table name.
409
	 * @param string $index The index name.
410
	 * @param string|array|boolean $spec The specification of the index in any
411
	 * loose format. See requireTable() for more information.
412
	 */
413
	public function requireIndex($table, $index, $spec) {
414
		// Detect if adding to a new table
415
		$newTable = !isset($this->tableList[strtolower($table)]);
416
417
		// Force spec into standard array format
418
		$spec = $this->parseIndexSpec($index, $spec);
419
		$specString = $this->convertIndexSpec($spec);
420
421
		// Check existing index
422
		$oldSpecString = null;
423
		$indexKey = null;
424
		if (!$newTable) {
425
			$indexKey = $this->indexKey($table, $index, $spec);
426
			$indexList = $this->indexList($table);
427
			if (isset($indexList[$indexKey])) {
428
429
				// $oldSpec should be in standard array format
430
				$oldSpec = $indexList[$indexKey];
431
				$oldSpecString = $this->convertIndexSpec($oldSpec);
432
			}
433
		}
434
435
		// Initiate either generation or modification of index
436
		if ($newTable || !isset($indexList[$indexKey])) {
437
			// New index
438
			$this->transCreateIndex($table, $index, $spec);
439
			$this->alterationMessage("Index $table.$index: created as $specString", "created");
440
		} else if ($oldSpecString != $specString) {
441
			// Updated index
442
			$this->transAlterIndex($table, $index, $spec);
443
			$this->alterationMessage(
444
				"Index $table.$index: changed to $specString <i style=\"color: #AAA\">(from $oldSpecString)</i>",
445
				"changed"
446
			);
447
		}
448
	}
449
450
	/**
451
	 * Splits a spec string safely, considering quoted columns, whitespace,
452
	 * and cleaning brackets
453
	 *
454
	 * @param string $spec The input index specification string
455
	 * @return array List of columns in the spec
456
	 */
457
	protected function explodeColumnString($spec) {
458
		// Remove any leading/trailing brackets and outlying modifiers
459
		// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
460
		$containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
461
462
		// Split potentially quoted modifiers
463
		// 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...
464
		return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
465
	}
466
467
	/**
468
	 * Builds a properly quoted column list from an array
469
	 *
470
	 * @param array $columns List of columns to implode
471
	 * @return string A properly quoted list of column names
472
	 */
473
	protected function implodeColumnList($columns) {
474
		if(empty($columns)) return '';
475
		return '"' . implode('","', $columns) . '"';
476
	}
477
478
	/**
479
	 * Given an index specification in the form of a string ensure that each
480
	 * column name is property quoted, stripping brackets and modifiers.
481
	 * This index may also be in the form of a "CREATE INDEX..." sql fragment
482
	 *
483
	 * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
484
	 * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
485
	 */
486
	protected function quoteColumnSpecString($spec) {
487
		$bits = $this->explodeColumnString($spec);
488
		return $this->implodeColumnList($bits);
489
	}
490
491
	/**
492
	 * Given an index spec determines the index type
493
	 *
494
	 * @param array|string $spec
495
	 * @return string
496
	 */
497
	protected function determineIndexType($spec) {
498
		// check array spec
499
		if(is_array($spec) && isset($spec['type'])) {
500
			return $spec['type'];
501
		} elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
502
			return strtolower($matchType['type']);
503
		} else {
504
			return 'index';
505
		}
506
	}
507
508
	/**
509
	 * Converts an array or string index spec into a universally useful array
510
	 *
511
	 * @see convertIndexSpec() for approximate inverse
512
	 * @param string $name Index name
513
	 * @param string|array $spec
514
	 * @return array The resulting spec array with the required fields name, type, and value
515
	 */
516
	protected function parseIndexSpec($name, $spec) {
517
		// Support $indexes = array('ColumnName' => true) for quick indexes
518
		if ($spec === true) {
519
			return array(
520
				'name' => $name,
521
				'value' => $this->quoteColumnSpecString($name),
522
				'type' => 'index'
523
			);
524
		}
525
526
		// Do minimal cleanup on any already parsed spec
527
		if(is_array($spec)) {
528
			$spec['value'] = $this->quoteColumnSpecString($spec['value']);
529
			$spec['type'] = empty($spec['type']) ? 'index' : trim($spec['type']);
530
			return $spec;
531
		}
532
533
		// Nicely formatted spec!
534
		return array(
535
			'name' => $name,
536
			'value' => $this->quoteColumnSpecString($spec),
537
			'type' => $this->determineIndexType($spec)
538
		);
539
	}
540
541
	/**
542
	 * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
543
	 * and turns it into a proper string.
544
	 * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
545
	 * arrays to be created. See {@link requireTable()} for details on the index format.
546
	 *
547
	 * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
548
	 * @see parseIndexSpec() for approximate inverse
549
	 *
550
	 * @param string|array $indexSpec
551
	 * @return string
552
	 */
553
	protected function convertIndexSpec($indexSpec) {
554
		// Return already converted spec
555
		if (!is_array($indexSpec)) {
556
			return $indexSpec;
557
		}
558
559
		// Combine elements into standard string format
560
		return "{$indexSpec['type']} ({$indexSpec['value']})";
561
	}
562
563
	/**
564
	 * Returns true if the given table is exists in the current database
565
	 *
566
	 * @param string $tableName Name of table to check
567
	 * @return boolean Flag indicating existence of table
568
	 */
569
	abstract public function hasTable($tableName);
570
571
	/**
572
	 * Return true if the table exists and already has a the field specified
573
	 *
574
	 * @param string $tableName - The table to check
575
	 * @param string $fieldName - The field to check
576
	 * @return bool - True if the table exists and the field exists on the table
577
	 */
578
	public function hasField($tableName, $fieldName) {
579
		if (!$this->hasTable($tableName)) return false;
580
		$fields = $this->fieldList($tableName);
581
		return array_key_exists($fieldName, $fields);
582
	}
583
584
	/**
585
	 * Generate the given field on the table, modifying whatever already exists as necessary.
586
	 *
587
	 * @param string $table The table name.
588
	 * @param string $field The field name.
589
	 * @param array|string $spec The field specification. If passed in array syntax, the specific database
590
	 * 	driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
591
	 * 	be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
592
	 * 	need to take care of database abstraction in your DBField subclass.
593
	 */
594
	public function requireField($table, $field, $spec) {
595
		//TODO: this is starting to get extremely fragmented.
596
		//There are two different versions of $spec floating around, and their content changes depending
597
		//on how they are structured.  This needs to be tidied up.
598
		$fieldValue = null;
599
		$newTable = false;
600
601
		// backwards compatibility patch for pre 2.4 requireField() calls
602
		$spec_orig = $spec;
603
604
		if (!is_string($spec)) {
605
			$spec['parts']['name'] = $field;
606
			$spec_orig['parts']['name'] = $field;
607
			//Convert the $spec array into a database-specific string
608
			$spec = $this->{$spec['type']}($spec['parts'], true);
609
		}
610
611
		// Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
612
		// collations.
613
		// TODO: move this to the MySQLDatabase file, or drop it altogether?
614
		if (!$this->database->supportsCollations()) {
615
			$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
616
		}
617
618
		if (!isset($this->tableList[strtolower($table)])) $newTable = true;
619
620
		if (is_array($spec)) {
621
			$specValue = $this->$spec_orig['type']($spec_orig['parts']);
622
		} else {
623
			$specValue = $spec;
624
		}
625
626
		// We need to get db-specific versions of the ID column:
627
		if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
628
			$specValue = $this->IdColumn(true);
629
		}
630
631
		if (!$newTable) {
632
			$fieldList = $this->fieldList($table);
633
			if (isset($fieldList[$field])) {
634
				if (is_array($fieldList[$field])) {
635
					$fieldValue = $fieldList[$field]['data_type'];
636
				} else {
637
					$fieldValue = $fieldList[$field];
638
				}
639
			}
640
		}
641
642
		// Get the version of the field as we would create it. This is used for comparison purposes to see if the
643
		// existing field is different to what we now want
644
		if (is_array($spec_orig)) {
645
			$spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
646
		}
647
648
		if ($newTable || $fieldValue == '') {
649
			$this->transCreateField($table, $field, $spec_orig);
650
			$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
651
		} else if ($fieldValue != $specValue) {
652
			// If enums/sets are being modified, then we need to fix existing data in the table.
653
			// Update any records where the enum is set to a legacy value to be set to the default.
654
			foreach (array('enum', 'set') as $enumtype) {
655
				if (preg_match("/^$enumtype/i", $specValue)) {
656
					$newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
657
					$new = preg_split("/'\\s*,\\s*'/", $newStr);
658
659
					$oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
660
					$old = preg_split("/'\\s*,\\s*'/", $oldStr);
661
662
					$holder = array();
663
					foreach ($old as $check) {
664
						if (!in_array($check, $new)) {
665
							$holder[] = $check;
666
						}
667
					}
668
					if (count($holder)) {
669
						$default = explode('default ', $spec_orig);
670
						$default = $default[1];
671
						$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
672
						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...
673
							$query .= "'{$holder[$i]}', ";
674
						}
675
						$query .= "'{$holder[$i]}')";
676
						$this->query($query);
677
						$amount = $this->database->affectedRows();
678
						$this->alterationMessage("Changed $amount rows to default value of field $field"
679
								. " (Value: $default)");
680
					}
681
				}
682
			}
683
			$this->transAlterField($table, $field, $spec_orig);
684
			$this->alterationMessage(
685
				"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
686
				"changed"
687
			);
688
		}
689
	}
690
691
	/**
692
	 * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
693
	 *
694
	 * @param string $table
695
	 * @param string $fieldName
696
	 */
697
	public function dontRequireField($table, $fieldName) {
698
		$fieldList = $this->fieldList($table);
699
		if (array_key_exists($fieldName, $fieldList)) {
700
			$suffix = '';
701
			while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
702
				$suffix = $suffix
703
						? ((int)$suffix + 1)
704
						: 2;
705
			}
706
			$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
707
			$this->alterationMessage(
708
				"Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
709
				"obsolete"
710
			);
711
		}
712
	}
713
714
	/**
715
	 * Show a message about database alteration
716
	 *
717
	 * @param string $message to display
718
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
719
	 */
720
	public function alterationMessage($message, $type = "") {
721
		if (!$this->supressOutput) {
722
			if (Director::is_cli()) {
723
				switch ($type) {
724
					case "created":
725
					case "changed":
726
					case "repaired":
727
						$sign = "+";
728
						break;
729
					case "obsolete":
730
					case "deleted":
731
						$sign = '-';
732
						break;
733
					case "notice":
734
						$sign = '*';
735
						break;
736
					case "error":
737
						$sign = "!";
738
						break;
739
					default:
740
						$sign = " ";
741
				}
742
				$message = strip_tags($message);
743
				echo "  $sign $message\n";
744
			} else {
745
				switch ($type) {
746
					case "created":
747
						$color = "green";
748
						break;
749
					case "obsolete":
750
						$color = "red";
751
						break;
752
					case "notice":
753
						$color = "orange";
754
						break;
755
					case "error":
756
						$color = "red";
757
						break;
758
					case "deleted":
759
						$color = "red";
760
						break;
761
					case "changed":
762
						$color = "blue";
763
						break;
764
					case "repaired":
765
						$color = "blue";
766
						break;
767
					default:
768
						$color = "";
769
				}
770
				echo "<li style=\"color: $color\">$message</li>";
771
			}
772
		}
773
	}
774
775
	/**
776
	 * This returns the data type for the id column which is the primary key for each table
777
	 *
778
	 * @param boolean $asDbValue
779
	 * @param boolean $hasAutoIncPK
780
	 * @return string
781
	 */
782
	abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
783
784
	/**
785
	 * Checks a table's integrity and repairs it if necessary.
786
	 *
787
	 * @param string $tableName The name of the table.
788
	 * @return boolean Return true if the table has integrity after the method is complete.
789
	 */
790
	abstract public function checkAndRepairTable($tableName);
791
792
793
	/**
794
	 * Ensure the given table has the correct case
795
	 *
796
	 * @param string $tableName Name of table in desired case
797
	 */
798
	public function fixTableCase($tableName)
799
	{
800
		// Check if table exists
801
		$tables = $this->tableList();
802
		if (!array_key_exists(strtolower($tableName), $tables)) {
803
			return;
804
		}
805
806
		// Check if case differs
807
		$currentName = $tables[strtolower($tableName)];
808
		if ($currentName === $tableName) {
809
			return;
810
		}
811
812
		$this->alterationMessage(
813
			"Table $tableName: renamed from $currentName",
814
			"repaired"
815
		);
816
817
		// Rename via temp table to avoid case-sensitivity issues
818
		$tempTable = "__TEMP__{$tableName}";
819
		$this->renameTable($currentName, $tempTable);
820
		$this->renameTable($tempTable, $tableName);
821
	}
822
823
	/**
824
	 * Returns the values of the given enum field
825
	 *
826
	 * @param string $tableName Name of table to check
827
	 * @param string $fieldName name of enum field to check
828
	 * @return array List of enum values
829
	 */
830
	abstract public function enumValuesForField($tableName, $fieldName);
831
832
833
	/*
834
	 * This is a lookup table for data types.
835
	 * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
836
	 * So this is a DB-specific list of equivilents.
837
	 *
838
	 * @param string $type
839
	 * @return string
840
	 */
841
	abstract public function dbDataType($type);
842
843
	/**
844
	 * Retrieves the list of all databases the user has access to
845
	 *
846
	 * @return array List of database names
847
	 */
848
	abstract public function databaseList();
849
850
	/**
851
	 * Determine if the database with the specified name exists
852
	 *
853
	 * @param string $name Name of the database to check for
854
	 * @return boolean Flag indicating whether this database exists
855
	 */
856
	abstract public function databaseExists($name);
857
858
	/**
859
	 * Create a database with the specified name
860
	 *
861
	 * @param string $name Name of the database to create
862
	 * @return boolean True if successful
863
	 */
864
	abstract public function createDatabase($name);
865
866
	/**
867
	 * Drops a database with the specified name
868
	 *
869
	 * @param string $name Name of the database to drop
870
	 */
871
	abstract public function dropDatabase($name);
872
873
	/**
874
	 * Alter an index on a table.
875
	 *
876
	 * @param string $tableName The name of the table.
877
	 * @param string $indexName The name of the index.
878
	 * @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
879
	 *                          for more details.
880
	 * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
881
	 */
882
	abstract public function alterIndex($tableName, $indexName, $indexSpec);
883
884
	/**
885
	 * Determines the key that should be used to identify this index
886
	 * when retrieved from DBSchemaManager->indexList.
887
	 * In some connectors this is the database-visible name, in others the
888
	 * usercode-visible name.
889
	 *
890
	 * @param string $table
891
	 * @param string $index
892
	 * @param array $spec
893
	 * @return string Key for this index
894
	 */
895
	abstract protected function indexKey($table, $index, $spec);
896
897
	/**
898
	 * Return the list of indexes in a table.
899
	 *
900
	 * @param string $table The table name.
901
	 * @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...
902
	 * array form. The key for this array should be predictable using the indexKey
903
	 * method
904
	 */
905
	abstract public function indexList($table);
906
907
	/**
908
	 * Returns a list of all tables in the database.
909
	 * Keys are table names in lower case, values are table names in case that
910
	 * database expects.
911
	 *
912
	 * @return array
913
	 */
914
	abstract public function tableList();
915
916
	/**
917
	 * Create a new table.
918
	 *
919
	 * @param string $table The name of the table
920
	 * @param array $fields A map of field names to field types
921
	 * @param array $indexes A map of indexes
922
	 * @param array $options An map of additional options.  The available keys are as follows:
923
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
924
	 *   - 'temporary' - If true, then a temporary table will be created
925
	 * @param array $advancedOptions Advanced creation options
926
	 * @return string The table name generated.  This may be different from the table name, for example with temporary
927
	 * tables.
928
	 */
929
	abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
930
										$advancedOptions = null);
931
932
	/**
933
	 * Alter a table's schema.
934
	 *
935
	 * @param string $table The name of the table to alter
936
	 * @param array $newFields New fields, a map of field name => field schema
937
	 * @param array $newIndexes New indexes, a map of index name => index type
938
	 * @param array $alteredFields Updated fields, a map of field name => field schema
939
	 * @param array $alteredIndexes Updated indexes, a map of index name => index type
940
	 * @param array $alteredOptions
941
	 * @param array $advancedOptions
942
	 */
943
	abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
944
										$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null);
945
946
	/**
947
	 * Rename a table.
948
	 *
949
	 * @param string $oldTableName The old table name.
950
	 * @param string $newTableName The new table name.
951
	 */
952
	abstract public function renameTable($oldTableName, $newTableName);
953
954
	/**
955
	 * Create a new field on a table.
956
	 *
957
	 * @param string $table Name of the table.
958
	 * @param string $field Name of the field to add.
959
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
960
	 */
961
	abstract public function createField($table, $field, $spec);
962
963
	/**
964
	 * Change the database column name of the given field.
965
	 *
966
	 * @param string $tableName The name of the tbale the field is in.
967
	 * @param string $oldName The name of the field to change.
968
	 * @param string $newName The new name of the field
969
	 */
970
	abstract public function renameField($tableName, $oldName, $newName);
971
972
	/**
973
	 * Get a list of all the fields for the given table.
974
	 * Returns a map of field name => field spec.
975
	 *
976
	 * @param string $table The table name.
977
	 * @return array
978
	 */
979
	abstract public function fieldList($table);
980
981
	/**
982
	 *
983
	 * This allows the cached values for a table's field list to be erased.
984
	 * If $tablename is empty, then the whole cache is erased.
985
	 *
986
	 * @param string $tableName
987
	 * @return boolean
988
	 */
989
	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...
990
		return true;
991
	}
992
993
994
	/**
995
	 * Returns data type for 'boolean' column
996
	 *
997
	 * @param array $values Contains a tokenised list of info about this data type
998
	 * @return string
999
	 */
1000
	abstract public function boolean($values);
1001
1002
	/**
1003
	 * Returns data type for 'date' column
1004
	 *
1005
	 * @param array $values Contains a tokenised list of info about this data type
1006
	 * @return string
1007
	 */
1008
	abstract public function date($values);
1009
1010
	/**
1011
	 * Returns data type for 'decimal' column
1012
	 *
1013
	 * @param array $values Contains a tokenised list of info about this data type
1014
	 * @return string
1015
	 */
1016
	abstract public function decimal($values);
1017
1018
	/**
1019
	 * Returns data type for 'set' column
1020
	 *
1021
	 * @param array $values Contains a tokenised list of info about this data type
1022
	 * @return string
1023
	 */
1024
	abstract public function enum($values);
1025
1026
	/**
1027
	 * Returns data type for 'set' column
1028
	 *
1029
	 * @param array $values Contains a tokenised list of info about this data type
1030
	 * @return string
1031
	 */
1032
	abstract public function set($values);
1033
1034
	/**
1035
	 * Returns data type for 'float' column
1036
	 *
1037
	 * @param array $values Contains a tokenised list of info about this data type
1038
	 * @return string
1039
	 */
1040
	abstract public function float($values);
1041
1042
	/**
1043
	 * Returns data type for 'int' column
1044
	 *
1045
	 * @param array $values Contains a tokenised list of info about this data type
1046
	 * @return string
1047
	 */
1048
	abstract public function int($values);
1049
1050
	/**
1051
	 * Returns data type for 'datetime' column
1052
	 *
1053
	 * @param array $values Contains a tokenised list of info about this data type
1054
	 * @return string
1055
	 */
1056
	abstract public function datetime($values);
1057
1058
	/**
1059
	 * Returns data type for 'text' column
1060
	 *
1061
	 * @param array $values Contains a tokenised list of info about this data type
1062
	 * @return string
1063
	 */
1064
	abstract public function text($values);
1065
1066
	/**
1067
	 * Returns data type for 'time' column
1068
	 *
1069
	 * @param array $values Contains a tokenised list of info about this data type
1070
	 * @return string
1071
	 */
1072
	abstract public function time($values);
1073
1074
	/**
1075
	 * Returns data type for 'varchar' column
1076
	 *
1077
	 * @param array $values Contains a tokenised list of info about this data type
1078
	 * @return string
1079
	 */
1080
	abstract public function varchar($values);
1081
1082
	/*
1083
	 * Returns data type for 'year' column
1084
	 *
1085
	 * @param array $values Contains a tokenised list of info about this data type
1086
	 * @return string
1087
	 */
1088
	abstract public function year($values);
1089
1090
}
1091