TYPO3 CMS  TYPO3_8-7
SqlSchemaMigrationService.php
Go to the documentation of this file.
1 <?php
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
20 
26 {
31 
35  protected $deletedPrefixKey = 'zzz_deleted_';
36 
40  protected $character_sets = [];
41 
47  public function setDeletedPrefixKey($prefix)
48  {
49  $this->deletedPrefixKey = $prefix;
50  }
51 
57  public function getDeletedPrefixKey()
58  {
60  }
61 
68  public function getFieldDefinitions_fileContent($fileContent)
69  {
70  $lines = GeneralUtility::trimExplode(LF, $fileContent, true);
71  $table = '';
72  $total = [];
73  foreach ($lines as $value) {
74  if ($value[0] === '#') {
75  // Ignore comments
76  continue;
77  }
78  if ($table === '') {
79  $parts = GeneralUtility::trimExplode(' ', $value, true);
80  if (strtoupper($parts[0]) === 'CREATE' && strtoupper($parts[1]) === 'TABLE') {
81  $table = str_replace('`', '', $parts[2]);
82  // tablenames are always lowercase on windows!
83  if (TYPO3_OS === 'WIN') {
84  $table = strtolower($table);
85  }
86  }
87  } else {
88  if ($value[0] === ')' && substr($value, -1) === ';') {
89  $ttype = [];
90  if (preg_match('/(ENGINE|TYPE)[ ]*=[ ]*([a-zA-Z]*)/', $value, $ttype)) {
91  $total[$table]['extra']['ENGINE'] = $ttype[2];
92  }
93  // Otherwise, just do nothing: If table engine is not defined, just accept the system default.
94  // Set the collation, if specified
95  if (preg_match('/(COLLATE)[ ]*=[ ]*([a-zA-z0-9_-]+)/', $value, $tcollation)) {
96  $total[$table]['extra']['COLLATE'] = $tcollation[2];
97  } else {
98  // Otherwise, get the CHARACTER SET and try to find the default collation for it as returned by "SHOW CHARACTER SET" query (for details, see http://dev.mysql.com/doc/refman/5.1/en/charset-table.html)
99  if (preg_match('/(CHARSET|CHARACTER SET)[ ]*=[ ]*([a-zA-z0-9_-]+)/', $value, $tcharset)) {
100  // Note: Keywords "DEFAULT CHARSET" and "CHARSET" are the same, so "DEFAULT" can just be ignored
101  $charset = $tcharset[2];
102  } else {
103  $charset = 'utf8';
104  }
105  $total[$table]['extra']['COLLATE'] = $this->getCollationForCharset($charset);
106  }
107  // Remove table marker and start looking for the next "CREATE TABLE" statement
108  $table = '';
109  } else {
110  // Strip trailing commas
111  $lineV = preg_replace('/,$/', '', $value);
112  $lineV = str_replace('`', '', $lineV);
113  // Reduce multiple blanks and tabs except newline
114  $lineV = preg_replace('/\h+/', ' ', $lineV);
115  $parts = explode(' ', $lineV, 2);
116  // Field definition
117  if (!preg_match('/(PRIMARY|UNIQUE|FULLTEXT|SPATIAL|INDEX|KEY)/', $parts[0])) {
118  // Make sure there is no default value when auto_increment is set
119  if (stristr($parts[1], 'auto_increment')) {
120  $parts[1] = preg_replace('/ default \'0\'/i', '', $parts[1]);
121  }
122  // "default" is always lower-case
123  if (stristr($parts[1], ' DEFAULT ')) {
124  $parts[1] = str_ireplace(' DEFAULT ', ' default ', $parts[1]);
125  }
126  // Change order of "default" and "NULL" statements
127  $parts[1] = preg_replace('/(.*) (default .*) (NOT NULL)/', '$1 $3 $2', $parts[1]);
128  $parts[1] = preg_replace('/(.*) (default .*) (NULL)/', '$1 $3 $2', $parts[1]);
129  $key = $parts[0];
130  $total[$table]['fields'][$key] = $parts[1];
131  } else {
132  // Key definition
133  $search = ['/UNIQUE (INDEX|KEY)/', '/FULLTEXT (INDEX|KEY)/', '/SPATIAL (INDEX|KEY)/', '/INDEX/'];
134  $replace = ['UNIQUE', 'FULLTEXT', 'SPATIAL', 'KEY'];
135  $lineV = preg_replace($search, $replace, $lineV);
136  if (preg_match('/PRIMARY|UNIQUE|FULLTEXT|SPATIAL/', $parts[0])) {
137  $parts[1] = preg_replace('/^(KEY|INDEX) /', '', $parts[1]);
138  }
139  $newParts = explode(' ', $parts[1], 2);
140  $key = $parts[0] === 'PRIMARY' ? $parts[0] : $newParts[0];
141  $total[$table]['keys'][$key] = $lineV;
142  // This is a protection against doing something stupid: Only allow clearing of cache_* and index_* tables.
143  if (preg_match('/^(cache|index)_/', $table)) {
144  // Suggest to truncate (clear) this table
145  $total[$table]['extra']['CLEAR'] = 1;
146  }
147  }
148  }
149  }
150  }
151  return $total;
152  }
153 
160  public function getCollationForCharset($charset)
161  {
162  // Load character sets, if not cached already
163  if (empty($this->character_sets)) {
164  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default');
165  $statement = $connection->query('SHOW CHARACTER SET');
166  $this->character_sets = [];
167  while ($row = $statement->fetch()) {
168  $this->character_sets[$row['Charset']] = $row;
169  }
170  }
171  $collation = '';
172  if (isset($this->character_sets[$charset]['Default collation'])) {
173  $collation = $this->character_sets[$charset]['Default collation'];
174  }
175  return $collation;
176  }
177 
184  {
185  $total = [];
186  $tempKeys = [];
187  $tempKeysPrefix = [];
188  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default');
189  $statement = $connection->query('SHOW TABLE STATUS FROM `' . $connection->getDatabase() . '`');
190  $tables = [];
191  while ($theTable = $statement->fetch()) {
192  $tables[$theTable['Name']] = $theTable;
193  }
194  foreach ($tables as $tableName => $tableStatus) {
195  // Fields
196  $statement = $connection->query('SHOW FULL COLUMNS FROM `' . $tableName . '`');
197  $fieldInformation = [];
198  while ($fieldRow = $statement->fetch()) {
199  $fieldInformation[$fieldRow['Field']] = $fieldRow;
200  }
201  foreach ($fieldInformation as $fN => $fieldRow) {
202  $total[$tableName]['fields'][$fN] = $this->assembleFieldDefinition($fieldRow);
203  }
204  // Keys
205  $statement = $connection->query('SHOW KEYS FROM `' . $tableName . '`');
206  $keyInformation = [];
207  while ($keyRow = $statement->fetch()) {
208  $keyInformation[] = $keyRow;
209  }
210  foreach ($keyInformation as $keyRow) {
211  $keyName = $keyRow['Key_name'];
212  $colName = $keyRow['Column_name'];
213  if ($keyRow['Sub_part'] && $keyRow['Index_type'] !== 'SPATIAL') {
214  $colName .= '(' . $keyRow['Sub_part'] . ')';
215  }
216  $tempKeys[$tableName][$keyName][$keyRow['Seq_in_index']] = $colName;
217  if ($keyName === 'PRIMARY') {
218  $prefix = 'PRIMARY KEY';
219  } else {
220  if ($keyRow['Index_type'] === 'FULLTEXT') {
221  $prefix = 'FULLTEXT';
222  } elseif ($keyRow['Index_type'] === 'SPATIAL') {
223  $prefix = 'SPATIAL';
224  } elseif ($keyRow['Non_unique']) {
225  $prefix = 'KEY';
226  } else {
227  $prefix = 'UNIQUE';
228  }
229  $prefix .= ' ' . $keyName;
230  }
231  $tempKeysPrefix[$tableName][$keyName] = $prefix;
232  }
233  // Table status (storage engine, collaction, etc.)
234  if (is_array($tableStatus)) {
235  $tableExtraFields = [
236  'Engine' => 'ENGINE',
237  'Collation' => 'COLLATE'
238  ];
239  foreach ($tableExtraFields as $mysqlKey => $internalKey) {
240  if (isset($tableStatus[$mysqlKey])) {
241  $total[$tableName]['extra'][$internalKey] = $tableStatus[$mysqlKey];
242  }
243  }
244  }
245  }
246  // Compile key information:
247  if (!empty($tempKeys)) {
248  foreach ($tempKeys as $table => $keyInf) {
249  foreach ($keyInf as $kName => $index) {
250  ksort($index);
251  $total[$table]['keys'][$kName] = $tempKeysPrefix[$table][$kName] . ' (' . implode(',', $index) . ')';
252  }
253  }
254  }
255  return $total;
256  }
257 
268  public function getDatabaseExtra($FDsrc, $FDcomp, $onlyTableList = '', $ignoreNotNullWhenComparing = false)
269  {
270  $extraArr = [];
271  $diffArr = [];
272  if (is_array($FDsrc)) {
273  foreach ($FDsrc as $table => $info) {
274  if ($onlyTableList === '' || GeneralUtility::inList($onlyTableList, $table)) {
275  if (!isset($FDcomp[$table])) {
276  // If the table was not in the FDcomp-array, the result array is loaded with that table.
277  $extraArr[$table] = $info;
278  $extraArr[$table]['whole_table'] = 1;
279  } else {
280  $keyTypes = explode(',', 'extra,fields,keys');
281  foreach ($keyTypes as $theKey) {
282  if (is_array($info[$theKey])) {
283  foreach ($info[$theKey] as $fieldN => $fieldC) {
284  $fieldN = str_replace('`', '', $fieldN);
285  if ($fieldN === 'COLLATE') {
286  // @todo collation support is currently disabled (needs more testing)
287  continue;
288  }
289  if (!isset($FDcomp[$table][$theKey][$fieldN])) {
290  $extraArr[$table][$theKey][$fieldN] = $fieldC;
291  } else {
292  $fieldC = trim($fieldC);
293 
294  // Lowercase the field type to surround false-positive schema changes to be
295  // reported just because of different caseing of characters
296  // The regex does just trigger for the first word followed by parentheses
297  // that contain a length. It does not trigger for e.g. "PRIMARY KEY" because
298  // "PRIMARY KEY" is being returned from the DB in upper case.
299  $fieldC = preg_replace_callback(
300  '/^([a-zA-Z0-9]+)(\([^)]*\)\s.*)/',
301  function ($matches) {
302  return strtolower($matches[1]) . $matches[2];
303  },
304  $fieldC
305  );
306 
307  if ($ignoreNotNullWhenComparing) {
308  $fieldC = str_replace(' NOT NULL', '', $fieldC);
309  $FDcomp[$table][$theKey][$fieldN] = str_replace(' NOT NULL', '', $FDcomp[$table][$theKey][$fieldN]);
310  }
311  if ($fieldC !== $FDcomp[$table][$theKey][$fieldN]) {
312  $diffArr[$table][$theKey][$fieldN] = $fieldC;
313  $diffArr_cur[$table][$theKey][$fieldN] = $FDcomp[$table][$theKey][$fieldN];
314  }
315  }
316  }
317  }
318  }
319  }
320  }
321  }
322  }
323  $output = [
324  'extra' => $extraArr,
325  'diff' => $diffArr,
326  'diff_currentValues' => $diffArr_cur
327  ];
328  return $output;
329  }
330 
338  public function getUpdateSuggestions($diffArr, $keyList = 'extra,diff')
339  {
340  $statements = [];
342  $deletedPrefixLength = strlen($deletedPrefixKey);
343  $remove = 0;
344  if ($keyList === 'remove') {
345  $remove = 1;
346  $keyList = 'extra';
347  }
348  $keyList = explode(',', $keyList);
349  foreach ($keyList as $theKey) {
350  if (is_array($diffArr[$theKey])) {
351  foreach ($diffArr[$theKey] as $table => $info) {
352  $whole_table = [];
353  if (isset($info['keys']) && is_array($info['keys'])) {
354  foreach ($info['keys'] as $fN => $fV) {
355  if (!$info['whole_table'] && $theKey === 'extra' && $remove) {
356  $statement = 'ALTER TABLE ' . $table . ($fN === 'PRIMARY' ? ' DROP PRIMARY KEY' : ' DROP KEY ' . $fN) . ';';
357  $statements['drop'][md5($statement)] = $statement;
358  }
359  }
360  }
361  if (is_array($info['fields'])) {
362  foreach ($info['fields'] as $fN => $fV) {
363  if ($info['whole_table']) {
364  $whole_table[] = $fN . ' ' . $fV;
365  } else {
366  // Special case to work around MySQL problems when adding auto_increment fields:
367  if (stristr($fV, 'auto_increment')) {
368  // The field can only be set "auto_increment" if there exists a PRIMARY key of that field already.
369  // The check does not look up which field is primary but just assumes it must be the field with the auto_increment value...
370  if (isset($info['keys']['PRIMARY'])) {
371  // Combine adding the field and the primary key into a single statement
372  $fV .= ', ADD PRIMARY KEY (' . $fN . ')';
373  unset($info['keys']['PRIMARY']);
374  } else {
375  // In the next step, attempt to clear the table once again (2 = force)
376  $info['extra']['CLEAR'] = 2;
377  }
378  }
379  if ($theKey === 'extra') {
380  if ($remove) {
381  if (substr($fN, 0, $deletedPrefixLength) !== $deletedPrefixKey) {
382  // we've to make sure we don't exceed the maximal length
383  $prefixedFieldName = $deletedPrefixKey . substr($fN, ($deletedPrefixLength - self::MYSQL_MAXIMUM_FIELD_WIDTH));
384  $statement = 'ALTER TABLE ' . $table . ' CHANGE ' . $fN . ' ' . $prefixedFieldName . ' ' . $fV . ';';
385  $statements['change'][md5($statement)] = $statement;
386  } else {
387  $statement = 'ALTER TABLE ' . $table . ' DROP ' . $fN . ';';
388  $statements['drop'][md5($statement)] = $statement;
389  }
390  } else {
391  $statement = 'ALTER TABLE ' . $table . ' ADD ' . $fN . ' ' . $fV . ';';
392  $statements['add'][md5($statement)] = $statement;
393  }
394  } elseif ($theKey === 'diff') {
395  $statement = 'ALTER TABLE ' . $table . ' CHANGE ' . $fN . ' ' . $fN . ' ' . $fV . ';';
396  $statements['change'][md5($statement)] = $statement;
397  $statements['change_currentValue'][md5($statement)] = $diffArr['diff_currentValues'][$table]['fields'][$fN];
398  }
399  }
400  }
401  }
402  if (is_array($info['keys'])) {
403  foreach ($info['keys'] as $fN => $fV) {
404  if ($info['whole_table']) {
405  $whole_table[] = $fV;
406  } else {
407  if ($theKey === 'extra') {
408  if (!$remove) {
409  $statement = 'ALTER TABLE ' . $table . ' ADD ' . $fV . ';';
410  $statements['add'][md5($statement)] = $statement;
411  }
412  } elseif ($theKey === 'diff') {
413  $statement = 'ALTER TABLE ' . $table . ($fN === 'PRIMARY' ? ' DROP PRIMARY KEY' : ' DROP KEY ' . $fN) . ';';
414  $statements['change'][md5($statement)] = $statement;
415  $statement = 'ALTER TABLE ' . $table . ' ADD ' . $fV . ';';
416  $statements['change'][md5($statement)] = $statement;
417  }
418  }
419  }
420  }
421  if (is_array($info['extra'])) {
422  $extras = [];
423  $extras_currentValue = [];
424  $clear_table = false;
425  foreach ($info['extra'] as $fN => $fV) {
426  // Only consider statements which are missing in the database but don't remove existing properties
427  if (!$remove) {
428  if (!$info['whole_table']) {
429  // If the whole table is created at once, we take care of this later by imploding all elements of $info['extra']
430  if ($fN === 'CLEAR') {
431  // Truncate table must happen later, not now
432  // Valid values for CLEAR: 1=only clear if keys are missing, 2=clear anyway (force)
433  if (!empty($info['keys']) || $fV == 2) {
434  $clear_table = true;
435  }
436  continue;
437  }
438  $extras[] = $fN . '=' . $fV;
439  $extras_currentValue[] = $fN . '=' . $diffArr['diff_currentValues'][$table]['extra'][$fN];
440  }
441  }
442  }
443  if ($clear_table) {
444  $statement = 'TRUNCATE TABLE ' . $table . ';';
445  $statements['clear_table'][md5($statement)] = $statement;
446  }
447  if (!empty($extras)) {
448  $statement = 'ALTER TABLE ' . $table . ' ' . implode(' ', $extras) . ';';
449  $statements['change'][md5($statement)] = $statement;
450  $statements['change_currentValue'][md5($statement)] = implode(' ', $extras_currentValue);
451  }
452  }
453  if ($info['whole_table']) {
454  if ($remove) {
455  if (substr($table, 0, $deletedPrefixLength) !== $deletedPrefixKey) {
456  // we've to make sure we don't exceed the maximal length
457  $prefixedTableName = $deletedPrefixKey . substr($table, ($deletedPrefixLength - self::MYSQL_MAXIMUM_FIELD_WIDTH));
458  $statement = 'ALTER TABLE ' . $table . ' RENAME ' . $prefixedTableName . ';';
459  $statements['change_table'][md5($statement)] = $statement;
460  } else {
461  $statement = 'DROP TABLE ' . $table . ';';
462  $statements['drop_table'][md5($statement)] = $statement;
463  }
464  // Count
465  $count = GeneralUtility::makeInstance(ConnectionPool::class)
466  ->getConnectionByName('Default')
467  ->count('*', $table, []);
468  $statements['tables_count'][md5($statement)] = $count ? 'Records in table: ' . $count : '';
469  } else {
470  $statement = 'CREATE TABLE ' . $table . ' (
471 ' . implode(',
472 ', $whole_table) . '
473 )';
474  if ($info['extra']) {
475  foreach ($info['extra'] as $k => $v) {
476  if ($k === 'COLLATE' || $k === 'CLEAR') {
477  // Skip these special statements.
478  // @todo collation support is currently disabled (needs more testing)
479  continue;
480  }
481  // Add extra attributes like ENGINE, CHARSET, etc.
482  $statement .= ' ' . $k . '=' . $v;
483  }
484  }
485  $statement .= ';';
486  $statements['create_table'][md5($statement)] = $statement;
487  }
488  }
489  }
490  }
491  }
492 
493  return $statements;
494  }
495 
502  public function assembleFieldDefinition($row)
503  {
504  $field = [$row['Type']];
505  if ($row['Null'] === 'NO') {
506  $field[] = 'NOT NULL';
507  }
508  if (!strstr($row['Type'], 'blob') && !strstr($row['Type'], 'text')) {
509  // Add a default value if the field is not auto-incremented (these fields never have a default definition)
510  if (!stristr($row['Extra'], 'auto_increment')) {
511  if ($row['Default'] === null) {
512  $field[] = 'default NULL';
513  } else {
514  $field[] = 'default \'' . addslashes($row['Default']) . '\'';
515  }
516  }
517  }
518  if ($row['Extra']) {
519  $field[] = $row['Extra'];
520  }
521  if (trim($row['Comment']) !== '') {
522  $field[] = "COMMENT '" . $row['Comment'] . "'";
523  }
524  return implode(' ', $field);
525  }
526 
535  public function getStatementArray($sqlcode, $removeNonSQL = false, $query_regex = '')
536  {
537  $sqlcodeArr = explode(LF, $sqlcode);
538  // Based on the assumption that the sql-dump has
539  $statementArray = [];
540  $statementArrayPointer = 0;
541  foreach ($sqlcodeArr as $line => $lineContent) {
542  $lineContent = trim($lineContent);
543  $is_set = 0;
544  // Auto_increment fields cannot have a default value!
545  if (stristr($lineContent, 'auto_increment')) {
546  $lineContent = preg_replace('/ default \'0\'/i', '', $lineContent);
547  }
548  if (!$removeNonSQL || $lineContent !== '' && $lineContent[0] !== '#' && substr($lineContent, 0, 2) !== '--') {
549  // '--' is seen as mysqldump comments from server version 3.23.49
550  $statementArray[$statementArrayPointer] .= $lineContent;
551  $is_set = 1;
552  }
553  if (substr($lineContent, -1) === ';') {
554  if (isset($statementArray[$statementArrayPointer])) {
555  if (!trim($statementArray[$statementArrayPointer]) || $query_regex && !preg_match(('/' . $query_regex . '/i'), trim($statementArray[$statementArrayPointer]))) {
556  unset($statementArray[$statementArrayPointer]);
557  }
558  }
559  $statementArrayPointer++;
560  } elseif ($is_set) {
561  $statementArray[$statementArrayPointer] .= LF;
562  }
563  }
564  return $statementArray;
565  }
566 
574  public function getCreateTables($statements, $insertCountFlag = false)
575  {
576  $crTables = [];
577  $insertCount = [];
578  foreach ($statements as $line => $lineContent) {
579  $reg = [];
580  if (preg_match('/^create[[:space:]]*table[[:space:]]*[`]?([[:alnum:]_]*)[`]?/i', substr($lineContent, 0, 100), $reg)) {
581  $table = trim($reg[1]);
582  if ($table) {
583  // Table names are always lowercase on Windows!
584  if (TYPO3_OS === 'WIN') {
585  $table = strtolower($table);
586  }
587  $sqlLines = explode(LF, $lineContent);
588  foreach ($sqlLines as $k => $v) {
589  if (stristr($v, 'auto_increment')) {
590  $sqlLines[$k] = preg_replace('/ default \'0\'/i', '', $v);
591  }
592  }
593  $lineContent = implode(LF, $sqlLines);
594  $crTables[$table] = $lineContent;
595  }
596  } elseif ($insertCountFlag && preg_match('/^insert[[:space:]]*into[[:space:]]*[`]?([[:alnum:]_]*)[`]?/i', substr($lineContent, 0, 100), $reg)) {
597  $nTable = trim($reg[1]);
598  $insertCount[$nTable]++;
599  }
600  }
601  return [$crTables, $insertCount];
602  }
603 
611  public function getTableInsertStatements($statements, $table)
612  {
613  $outStatements = [];
614  foreach ($statements as $line => $lineContent) {
615  $reg = [];
616  if (preg_match('/^insert[[:space:]]*into[[:space:]]*[`]?([[:alnum:]_]*)[`]?/i', substr($lineContent, 0, 100), $reg)) {
617  $nTable = trim($reg[1]);
618  if ($nTable && $table === $nTable) {
619  $outStatements[] = $lineContent;
620  }
621  }
622  }
623  return $outStatements;
624  }
625 
633  public function performUpdateQueries($arr, $keyArr)
634  {
635  $result = [];
636  if (is_array($arr)) {
637  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default');
638  foreach ($arr as $key => $string) {
639  if (isset($keyArr[$key]) && $keyArr[$key]) {
640  try {
641  $connection->query($string);
642  } catch (DBALException $e) {
643  $result[$key] = $e->getMessage();
644  }
645  }
646  }
647  }
648  if (!empty($result)) {
649  return $result;
650  }
651  return true;
652  }
653 
660  public function getListOfTables()
661  {
662  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName('Default');
663  $statement = $connection->query('SHOW TABLE STATUS FROM `' . $connection->getDatabase() . '`');
664  $tables = [];
665  while ($theTable = $statement->fetch()) {
666  $tables[$theTable['Name']] = $theTable;
667  }
668  foreach ($tables as $key => &$value) {
669  $value = $key;
670  }
671  unset($value);
672  return $tables;
673  }
674 }
getDatabaseExtra($FDsrc, $FDcomp, $onlyTableList='', $ignoreNotNullWhenComparing=false)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static makeInstance($className,... $constructorArguments)
getStatementArray($sqlcode, $removeNonSQL=false, $query_regex='')