1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Core\Database\Schema; 19 20use Doctrine\DBAL\Exception as DBALException; 21use Doctrine\DBAL\Platforms\MySqlPlatform; 22use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform; 23use Doctrine\DBAL\Platforms\SqlitePlatform; 24use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform; 25use Doctrine\DBAL\Schema\Column; 26use Doctrine\DBAL\Schema\ColumnDiff; 27use Doctrine\DBAL\Schema\ForeignKeyConstraint; 28use Doctrine\DBAL\Schema\Index; 29use Doctrine\DBAL\Schema\Schema; 30use Doctrine\DBAL\Schema\SchemaConfig; 31use Doctrine\DBAL\Schema\SchemaDiff; 32use Doctrine\DBAL\Schema\Table; 33use TYPO3\CMS\Core\Database\Connection; 34use TYPO3\CMS\Core\Database\ConnectionPool; 35use TYPO3\CMS\Core\Database\Platform\PlatformInformation; 36use TYPO3\CMS\Core\Utility\GeneralUtility; 37 38/** 39 * Handling schema migrations per connection. 40 * 41 * @internal 42 */ 43class ConnectionMigrator 44{ 45 /** 46 * @var string Prefix of deleted tables 47 */ 48 protected $deletedPrefix = 'zzz_deleted_'; 49 50 /** 51 * @var Connection 52 */ 53 protected $connection; 54 55 /** 56 * @var string 57 */ 58 protected $connectionName; 59 60 /** 61 * @var Table[] 62 */ 63 protected $tables; 64 65 /** 66 * @param string $connectionName 67 * @param Table[] $tables 68 */ 69 public function __construct(string $connectionName, array $tables) 70 { 71 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 72 $this->connection = $connectionPool->getConnectionByName($connectionName); 73 $this->connectionName = $connectionName; 74 $this->tables = $tables; 75 } 76 77 /** 78 * @param string $connectionName 79 * @param Table[] $tables 80 * @return ConnectionMigrator 81 */ 82 public static function create(string $connectionName, array $tables) 83 { 84 return GeneralUtility::makeInstance( 85 static::class, 86 $connectionName, 87 $tables 88 ); 89 } 90 91 /** 92 * Return the raw Doctrine SchemaDiff object for the current connection. 93 * This diff contains all changes without any pre-processing. 94 * 95 * @return SchemaDiff 96 */ 97 public function getSchemaDiff(): SchemaDiff 98 { 99 return $this->buildSchemaDiff(false); 100 } 101 102 /** 103 * Compare current and expected schema definitions and provide updates 104 * suggestions in the form of SQL statements. 105 * 106 * @param bool $remove 107 * @return array 108 */ 109 public function getUpdateSuggestions(bool $remove = false): array 110 { 111 $schemaDiff = $this->buildSchemaDiff(); 112 113 if ($remove === false) { 114 return array_merge_recursive( 115 ['add' => [], 'create_table' => [], 'change' => [], 'change_currentValue' => []], 116 $this->getNewFieldUpdateSuggestions($schemaDiff), 117 $this->getNewTableUpdateSuggestions($schemaDiff), 118 $this->getChangedFieldUpdateSuggestions($schemaDiff), 119 $this->getChangedTableOptions($schemaDiff) 120 ); 121 } 122 return array_merge_recursive( 123 ['change' => [], 'change_table' => [], 'drop' => [], 'drop_table' => [], 'tables_count' => []], 124 $this->getUnusedFieldUpdateSuggestions($schemaDiff), 125 $this->getUnusedTableUpdateSuggestions($schemaDiff), 126 $this->getDropTableUpdateSuggestions($schemaDiff), 127 $this->getDropFieldUpdateSuggestions($schemaDiff) 128 ); 129 } 130 131 /** 132 * Perform add/change/create operations on tables and fields in an 133 * optimized, non-interactive, mode using the original doctrine 134 * SchemaManager ->toSaveSql() method. 135 * 136 * @param bool $createOnly 137 * @return array 138 */ 139 public function install(bool $createOnly = false): array 140 { 141 $result = []; 142 $schemaDiff = $this->buildSchemaDiff(false); 143 144 $schemaDiff->removedTables = []; 145 foreach ($schemaDiff->changedTables as $key => $changedTable) { 146 $schemaDiff->changedTables[$key]->removedColumns = []; 147 $schemaDiff->changedTables[$key]->removedIndexes = []; 148 149 // With partial ext_tables.sql files the SchemaManager is detecting 150 // existing columns as false positives for a column rename. In this 151 // context every rename is actually a new column. 152 foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) { 153 $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance( 154 Column::class, 155 $renamedColumn->getName(), 156 $renamedColumn->getType(), 157 array_diff_key($renamedColumn->toArray(), ['name', 'type']) 158 ); 159 unset($changedTable->renamedColumns[$columnName]); 160 } 161 162 if ($createOnly) { 163 // Ignore new indexes that work on columns that need changes 164 foreach ($changedTable->addedIndexes as $indexName => $addedIndex) { 165 $indexColumns = array_map( 166 static function ($columnName) { 167 // Strip MySQL prefix length information to get real column names 168 $columnName = preg_replace('/\(\d+\)$/', '', $columnName) ?? ''; 169 // Strip mssql '[' and ']' from column names 170 $columnName = ltrim($columnName, '['); 171 $columnName = rtrim($columnName, ']'); 172 // Strip sqlite '"' from column names 173 return trim($columnName, '"'); 174 }, 175 $addedIndex->getColumns() 176 ); 177 $columnChanges = array_intersect($indexColumns, array_keys($changedTable->changedColumns)); 178 if (!empty($columnChanges)) { 179 unset($schemaDiff->changedTables[$key]->addedIndexes[$indexName]); 180 } 181 } 182 $schemaDiff->changedTables[$key]->changedColumns = []; 183 $schemaDiff->changedTables[$key]->changedIndexes = []; 184 $schemaDiff->changedTables[$key]->renamedIndexes = []; 185 } 186 } 187 188 $statements = $schemaDiff->toSaveSql( 189 $this->connection->getDatabasePlatform() 190 ); 191 192 foreach ($statements as $statement) { 193 try { 194 $this->connection->executeStatement($statement); 195 $result[$statement] = ''; 196 } catch (DBALException $e) { 197 $result[$statement] = $e->getPrevious()->getMessage(); 198 } 199 } 200 201 return $result; 202 } 203 204 /** 205 * If the schema is not for the Default connection remove all tables from the schema 206 * that have no mapping in the TYPO3 configuration. This avoids update suggestions 207 * for tables that are in the database but have no direct relation to the TYPO3 instance. 208 * 209 * @param bool $renameUnused 210 * @throws \Doctrine\DBAL\Exception 211 * @return \Doctrine\DBAL\Schema\SchemaDiff 212 * @throws \Doctrine\DBAL\Schema\SchemaException 213 * @throws \InvalidArgumentException 214 */ 215 protected function buildSchemaDiff(bool $renameUnused = true): SchemaDiff 216 { 217 // Unmapped tables in a non-default connection are ignored by TYPO3 218 $tablesForConnection = []; 219 if ($this->connectionName !== ConnectionPool::DEFAULT_CONNECTION_NAME) { 220 // If there are no mapped tables return a SchemaDiff without any changes 221 // to avoid update suggestions for tables not related to TYPO3. 222 if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) { 223 return new SchemaDiff(); 224 } 225 226 // Collect the table names that have been mapped to this connection. 227 $connectionName = $this->connectionName; 228 /** @var string[] $tablesForConnection */ 229 $tablesForConnection = array_keys( 230 array_filter( 231 $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'], 232 static function ($tableConnectionName) use ($connectionName) { 233 return $tableConnectionName === $connectionName; 234 } 235 ) 236 ); 237 238 // Ignore all tables without mapping if not in the default connection 239 $this->connection->getConfiguration()->setSchemaAssetsFilter( 240 static function ($assetName) use ($tablesForConnection) { 241 return in_array($assetName, $tablesForConnection, true); 242 } 243 ); 244 } 245 246 // Build the schema definitions 247 $fromSchema = $this->connection->createSchemaManager()->createSchema(); 248 $toSchema = $this->buildExpectedSchemaDefinitions($this->connectionName); 249 250 // Add current table options to the fromSchema 251 $tableOptions = $this->getTableOptions($fromSchema->getTableNames()); 252 foreach ($fromSchema->getTables() as $table) { 253 $tableName = $table->getName(); 254 if (!array_key_exists($tableName, $tableOptions)) { 255 continue; 256 } 257 foreach ($tableOptions[$tableName] as $optionName => $optionValue) { 258 $table->addOption($optionName, $optionValue); 259 } 260 } 261 262 // Build SchemaDiff and handle renames of tables and columns 263 $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform()); 264 $schemaDiff = $comparator->compare($fromSchema, $toSchema); 265 $schemaDiff = $this->migrateColumnRenamesToDistinctActions($schemaDiff); 266 267 if ($renameUnused) { 268 $schemaDiff = $this->migrateUnprefixedRemovedTablesToRenames($schemaDiff); 269 $schemaDiff = $this->migrateUnprefixedRemovedFieldsToRenames($schemaDiff); 270 } 271 272 // All tables in the default connection are managed by TYPO3 273 if ($this->connectionName === ConnectionPool::DEFAULT_CONNECTION_NAME) { 274 return $schemaDiff; 275 } 276 277 // Remove all tables that are not assigned to this connection from the diff 278 $schemaDiff->newTables = $this->removeUnrelatedTables($schemaDiff->newTables, $tablesForConnection); 279 $schemaDiff->changedTables = $this->removeUnrelatedTables($schemaDiff->changedTables, $tablesForConnection); 280 $schemaDiff->removedTables = $this->removeUnrelatedTables($schemaDiff->removedTables, $tablesForConnection); 281 282 return $schemaDiff; 283 } 284 285 /** 286 * Build the expected schema definitions from raw SQL statements. 287 * 288 * @param string $connectionName 289 * @return \Doctrine\DBAL\Schema\Schema 290 * @throws \Doctrine\DBAL\Exception 291 * @throws \InvalidArgumentException 292 */ 293 protected function buildExpectedSchemaDefinitions(string $connectionName): Schema 294 { 295 /** @var Table[] $tablesForConnection */ 296 $tablesForConnection = []; 297 foreach ($this->tables as $table) { 298 $tableName = $table->getName(); 299 300 // Skip tables for a different connection 301 if ($connectionName !== $this->getConnectionNameForTable($tableName)) { 302 continue; 303 } 304 305 if (!array_key_exists($tableName, $tablesForConnection)) { 306 $tablesForConnection[$tableName] = $table; 307 continue; 308 } 309 310 // Merge multiple table definitions. Later definitions overrule identical 311 // columns, indexes and foreign_keys. Order of definitions is based on 312 // extension load order. 313 $currentTableDefinition = $tablesForConnection[$tableName]; 314 $tablesForConnection[$tableName] = new Table( 315 $tableName, 316 array_merge($currentTableDefinition->getColumns(), $table->getColumns()), 317 array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()), 318 array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()), 319 0, 320 array_merge($currentTableDefinition->getOptions(), $table->getOptions()) 321 ); 322 } 323 324 $tablesForConnection = $this->transformTablesForDatabasePlatform($tablesForConnection, $this->connection); 325 326 $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class); 327 $schemaConfig->setName($this->connection->getDatabase()); 328 if (isset($this->connection->getParams()['tableoptions'])) { 329 $schemaConfig->setDefaultTableOptions($this->connection->getParams()['tableoptions']); 330 } 331 332 return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig); 333 } 334 335 /** 336 * Extract the update suggestions (SQL statements) for newly added tables 337 * from the complete schema diff. 338 * 339 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 340 * @return array 341 * @throws \InvalidArgumentException 342 */ 343 protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array 344 { 345 // Build a new schema diff that only contains added tables 346 $addTableSchemaDiff = GeneralUtility::makeInstance( 347 SchemaDiff::class, 348 $schemaDiff->newTables, 349 [], 350 [], 351 $schemaDiff->fromSchema 352 ); 353 354 $statements = $addTableSchemaDiff->toSql($this->connection->getDatabasePlatform()); 355 356 return ['create_table' => $this->calculateUpdateSuggestionsHashes($statements)]; 357 } 358 359 /** 360 * Extract the update suggestions (SQL statements) for newly added fields 361 * from the complete schema diff. 362 * 363 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 364 * @return array 365 * @throws \Doctrine\DBAL\Schema\SchemaException 366 * @throws \InvalidArgumentException 367 */ 368 protected function getNewFieldUpdateSuggestions(SchemaDiff $schemaDiff): array 369 { 370 $changedTables = []; 371 372 foreach ($schemaDiff->changedTables as $index => $changedTable) { 373 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name)); 374 375 if (count($changedTable->addedColumns) !== 0) { 376 // Treat each added column with a new diff to get a dedicated suggestions 377 // just for this single column. 378 foreach ($changedTable->addedColumns as $columnName => $addedColumn) { 379 $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance( 380 TableDiff::class, 381 $changedTable->name, 382 [$columnName => $addedColumn], 383 [], 384 [], 385 [], 386 [], 387 [], 388 $fromTable 389 ); 390 } 391 } 392 393 if (count($changedTable->addedIndexes) !== 0) { 394 // Treat each added index with a new diff to get a dedicated suggestions 395 // just for this index. 396 foreach ($changedTable->addedIndexes as $indexName => $addedIndex) { 397 $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance( 398 TableDiff::class, 399 $changedTable->name, 400 [], 401 [], 402 [], 403 [$indexName => $this->buildQuotedIndex($addedIndex)], 404 [], 405 [], 406 $fromTable 407 ); 408 } 409 } 410 411 if (count($changedTable->addedForeignKeys) !== 0) { 412 // Treat each added foreign key with a new diff to get a dedicated suggestions 413 // just for this foreign key. 414 foreach ($changedTable->addedForeignKeys as $addedForeignKey) { 415 $fkIndex = $index . ':fk_' . $addedForeignKey->getName(); 416 $changedTables[$fkIndex] = GeneralUtility::makeInstance( 417 TableDiff::class, 418 $changedTable->name, 419 [], 420 [], 421 [], 422 [], 423 [], 424 [], 425 $fromTable 426 ); 427 $changedTables[$fkIndex]->addedForeignKeys = [$this->buildQuotedForeignKey($addedForeignKey)]; 428 } 429 } 430 } 431 432 // Build a new schema diff that only contains added fields 433 $addFieldSchemaDiff = GeneralUtility::makeInstance( 434 SchemaDiff::class, 435 [], 436 $changedTables, 437 [], 438 $schemaDiff->fromSchema 439 ); 440 441 $statements = $addFieldSchemaDiff->toSql($this->connection->getDatabasePlatform()); 442 443 return ['add' => $this->calculateUpdateSuggestionsHashes($statements)]; 444 } 445 446 /** 447 * Extract update suggestions (SQL statements) for changed options 448 * (like ENGINE) from the complete schema diff. 449 * 450 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 451 * @return array 452 * @throws \Doctrine\DBAL\Schema\SchemaException 453 * @throws \InvalidArgumentException 454 */ 455 protected function getChangedTableOptions(SchemaDiff $schemaDiff): array 456 { 457 $updateSuggestions = []; 458 459 foreach ($schemaDiff->changedTables as $tableDiff) { 460 // Skip processing if this is the base TableDiff class or has no table options set. 461 if (!$tableDiff instanceof TableDiff || count($tableDiff->getTableOptions()) === 0) { 462 continue; 463 } 464 465 $tableOptions = $tableDiff->getTableOptions(); 466 $tableOptionsDiff = new TableDiff( 467 $tableDiff->name, 468 [], 469 [], 470 [], 471 [], 472 [], 473 [], 474 $tableDiff->fromTable 475 ); 476 $tableOptionsDiff->setTableOptions($tableOptions); 477 478 $tableOptionsSchemaDiff = GeneralUtility::makeInstance( 479 SchemaDiff::class, 480 [], 481 [$tableOptionsDiff], 482 [], 483 $schemaDiff->fromSchema 484 ); 485 486 $statements = $tableOptionsSchemaDiff->toSaveSql($this->connection->getDatabasePlatform()); 487 foreach ($statements as $statement) { 488 $updateSuggestions['change'][md5($statement)] = $statement; 489 } 490 } 491 492 return $updateSuggestions; 493 } 494 495 /** 496 * Extract update suggestions (SQL statements) for changed fields 497 * from the complete schema diff. 498 * 499 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 500 * @return array 501 * @throws \Doctrine\DBAL\Schema\SchemaException 502 * @throws \InvalidArgumentException 503 */ 504 protected function getChangedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array 505 { 506 $databasePlatform = $this->connection->getDatabasePlatform(); 507 $updateSuggestions = []; 508 509 foreach ($schemaDiff->changedTables as $index => $changedTable) { 510 // Treat each changed index with a new diff to get a dedicated suggestions 511 // just for this index. 512 if (count($changedTable->changedIndexes) !== 0) { 513 foreach ($changedTable->changedIndexes as $indexName => $changedIndex) { 514 $indexDiff = GeneralUtility::makeInstance( 515 TableDiff::class, 516 $changedTable->name, 517 [], 518 [], 519 [], 520 [], 521 [$indexName => $changedIndex], 522 [], 523 $schemaDiff->fromSchema->getTable($changedTable->name) 524 ); 525 526 $temporarySchemaDiff = GeneralUtility::makeInstance( 527 SchemaDiff::class, 528 [], 529 [$indexDiff], 530 [], 531 $schemaDiff->fromSchema 532 ); 533 534 $statements = $temporarySchemaDiff->toSql($databasePlatform); 535 foreach ($statements as $statement) { 536 $updateSuggestions['change'][md5($statement)] = $statement; 537 } 538 } 539 } 540 541 // Treat renamed indexes as a field change as it's a simple rename operation 542 if (count($changedTable->renamedIndexes) !== 0) { 543 // Create a base table diff without any changes, there's no constructor 544 // argument to pass in renamed indexes. 545 $tableDiff = GeneralUtility::makeInstance( 546 TableDiff::class, 547 $changedTable->name, 548 [], 549 [], 550 [], 551 [], 552 [], 553 [], 554 $schemaDiff->fromSchema->getTable($changedTable->name) 555 ); 556 557 // Treat each renamed index with a new diff to get a dedicated suggestions 558 // just for this index. 559 foreach ($changedTable->renamedIndexes as $key => $renamedIndex) { 560 $indexDiff = clone $tableDiff; 561 $indexDiff->renamedIndexes = [$key => $renamedIndex]; 562 563 $temporarySchemaDiff = GeneralUtility::makeInstance( 564 SchemaDiff::class, 565 [], 566 [$indexDiff], 567 [], 568 $schemaDiff->fromSchema 569 ); 570 571 $statements = $temporarySchemaDiff->toSql($databasePlatform); 572 foreach ($statements as $statement) { 573 $updateSuggestions['change'][md5($statement)] = $statement; 574 } 575 } 576 } 577 578 if (count($changedTable->changedColumns) !== 0) { 579 // Treat each changed column with a new diff to get a dedicated suggestions 580 // just for this single column. 581 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name)); 582 583 foreach ($changedTable->changedColumns as $columnName => $changedColumn) { 584 // Field has been renamed and will be handled separately 585 if ($changedColumn->getOldColumnName()->getName() !== $changedColumn->column->getName()) { 586 continue; 587 } 588 589 if ($changedColumn->fromColumn !== null) { 590 $changedColumn->fromColumn = $this->buildQuotedColumn($changedColumn->fromColumn); 591 } 592 593 // Get the current SQL declaration for the column 594 $currentColumn = $fromTable->getColumn($changedColumn->getOldColumnName()->getName()); 595 $currentDeclaration = $databasePlatform->getColumnDeclarationSQL( 596 $currentColumn->getQuotedName($this->connection->getDatabasePlatform()), 597 $currentColumn->toArray() 598 ); 599 600 // Build a dedicated diff just for the current column 601 $tableDiff = GeneralUtility::makeInstance( 602 TableDiff::class, 603 $changedTable->name, 604 [], 605 [$columnName => $changedColumn], 606 [], 607 [], 608 [], 609 [], 610 $fromTable 611 ); 612 613 $temporarySchemaDiff = GeneralUtility::makeInstance( 614 SchemaDiff::class, 615 [], 616 [$tableDiff], 617 [], 618 $schemaDiff->fromSchema 619 ); 620 621 $statements = $temporarySchemaDiff->toSql($databasePlatform); 622 foreach ($statements as $statement) { 623 $updateSuggestions['change'][md5($statement)] = $statement; 624 $updateSuggestions['change_currentValue'][md5($statement)] = $currentDeclaration; 625 } 626 } 627 } 628 629 // Treat each changed foreign key with a new diff to get a dedicated suggestions 630 // just for this foreign key. 631 if (count($changedTable->changedForeignKeys) !== 0) { 632 $tableDiff = GeneralUtility::makeInstance( 633 TableDiff::class, 634 $changedTable->name, 635 [], 636 [], 637 [], 638 [], 639 [], 640 [], 641 $schemaDiff->fromSchema->getTable($changedTable->name) 642 ); 643 644 foreach ($changedTable->changedForeignKeys as $changedForeignKey) { 645 $foreignKeyDiff = clone $tableDiff; 646 $foreignKeyDiff->changedForeignKeys = [$this->buildQuotedForeignKey($changedForeignKey)]; 647 648 $temporarySchemaDiff = GeneralUtility::makeInstance( 649 SchemaDiff::class, 650 [], 651 [$foreignKeyDiff], 652 [], 653 $schemaDiff->fromSchema 654 ); 655 656 $statements = $temporarySchemaDiff->toSql($databasePlatform); 657 foreach ($statements as $statement) { 658 $updateSuggestions['change'][md5($statement)] = $statement; 659 } 660 } 661 } 662 } 663 664 return $updateSuggestions; 665 } 666 667 /** 668 * Extract update suggestions (SQL statements) for tables that are 669 * no longer present in the expected schema from the schema diff. 670 * In this case the update suggestions are renames of the tables 671 * with a prefix to mark them for deletion in a second sweep. 672 * 673 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 674 * @return array 675 * @throws \Doctrine\DBAL\Schema\SchemaException 676 * @throws \InvalidArgumentException 677 */ 678 protected function getUnusedTableUpdateSuggestions(SchemaDiff $schemaDiff): array 679 { 680 $updateSuggestions = []; 681 foreach ($schemaDiff->changedTables as $tableDiff) { 682 // Skip tables that are not being renamed or where the new name isn't prefixed 683 // with the deletion marker. 684 if ($tableDiff->getNewName() === false 685 || strpos($tableDiff->getNewName()->getName(), $this->deletedPrefix) !== 0 686 ) { 687 continue; 688 } 689 // Build a new schema diff that only contains this table 690 $changedFieldDiff = GeneralUtility::makeInstance( 691 SchemaDiff::class, 692 [], 693 [$tableDiff], 694 [], 695 $schemaDiff->fromSchema 696 ); 697 698 $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform()); 699 700 foreach ($statements as $statement) { 701 $updateSuggestions['change_table'][md5($statement)] = $statement; 702 } 703 $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount((string)$tableDiff->name); 704 } 705 706 return $updateSuggestions; 707 } 708 709 /** 710 * Extract update suggestions (SQL statements) for fields that are 711 * no longer present in the expected schema from the schema diff. 712 * In this case the update suggestions are renames of the fields 713 * with a prefix to mark them for deletion in a second sweep. 714 * 715 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 716 * @return array 717 * @throws \Doctrine\DBAL\Schema\SchemaException 718 * @throws \InvalidArgumentException 719 */ 720 protected function getUnusedFieldUpdateSuggestions(SchemaDiff $schemaDiff): array 721 { 722 $changedTables = []; 723 724 foreach ($schemaDiff->changedTables as $index => $changedTable) { 725 if (count($changedTable->changedColumns) === 0) { 726 continue; 727 } 728 729 $databasePlatform = $this->getDatabasePlatform($index); 730 731 // Treat each changed column with a new diff to get a dedicated suggestions 732 // just for this single column. 733 foreach ($changedTable->changedColumns as $oldFieldName => $changedColumn) { 734 // Field has not been renamed 735 if ($changedColumn->getOldColumnName()->getName() === $changedColumn->column->getName()) { 736 continue; 737 } 738 739 $renameColumnTableDiff = GeneralUtility::makeInstance( 740 TableDiff::class, 741 $changedTable->name, 742 [], 743 [$oldFieldName => $changedColumn], 744 [], 745 [], 746 [], 747 [], 748 $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name)) 749 ); 750 if ($databasePlatform === 'postgresql') { 751 $renameColumnTableDiff->renamedColumns[$oldFieldName] = $changedColumn->column; 752 } 753 $changedTables[$index . ':' . $changedColumn->column->getName()] = $renameColumnTableDiff; 754 755 if ($databasePlatform === 'sqlite') { 756 break; 757 } 758 } 759 } 760 761 // Build a new schema diff that only contains unused fields 762 $changedFieldDiff = GeneralUtility::makeInstance( 763 SchemaDiff::class, 764 [], 765 $changedTables, 766 [], 767 $schemaDiff->fromSchema 768 ); 769 770 $statements = $changedFieldDiff->toSql($this->connection->getDatabasePlatform()); 771 772 return ['change' => $this->calculateUpdateSuggestionsHashes($statements)]; 773 } 774 775 /** 776 * Extract update suggestions (SQL statements) for fields that can 777 * be removed from the complete schema diff. 778 * Fields that can be removed have been prefixed in a previous run 779 * of the schema migration. 780 * 781 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 782 * @return array 783 * @throws \Doctrine\DBAL\Schema\SchemaException 784 * @throws \InvalidArgumentException 785 */ 786 protected function getDropFieldUpdateSuggestions(SchemaDiff $schemaDiff): array 787 { 788 $changedTables = []; 789 790 foreach ($schemaDiff->changedTables as $index => $changedTable) { 791 $fromTable = $this->buildQuotedTable($schemaDiff->fromSchema->getTable($changedTable->name)); 792 793 $isSqlite = $this->getDatabasePlatform($index) === 'sqlite'; 794 $addMoreOperations = true; 795 796 if (count($changedTable->removedColumns) !== 0) { 797 // Treat each changed column with a new diff to get a dedicated suggestions 798 // just for this single column. 799 foreach ($changedTable->removedColumns as $columnName => $removedColumn) { 800 $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance( 801 TableDiff::class, 802 $changedTable->name, 803 [], 804 [], 805 [$columnName => $this->buildQuotedColumn($removedColumn)], 806 [], 807 [], 808 [], 809 $fromTable 810 ); 811 if ($isSqlite) { 812 $addMoreOperations = false; 813 break; 814 } 815 } 816 } 817 818 if ($addMoreOperations && count($changedTable->removedIndexes) !== 0) { 819 // Treat each removed index with a new diff to get a dedicated suggestions 820 // just for this index. 821 foreach ($changedTable->removedIndexes as $indexName => $removedIndex) { 822 $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance( 823 TableDiff::class, 824 $changedTable->name, 825 [], 826 [], 827 [], 828 [], 829 [], 830 [$indexName => $this->buildQuotedIndex($removedIndex)], 831 $fromTable 832 ); 833 if ($isSqlite) { 834 $addMoreOperations = false; 835 break; 836 } 837 } 838 } 839 840 if ($addMoreOperations && count($changedTable->removedForeignKeys) !== 0) { 841 // Treat each removed foreign key with a new diff to get a dedicated suggestions 842 // just for this foreign key. 843 foreach ($changedTable->removedForeignKeys as $removedForeignKey) { 844 if (is_string($removedForeignKey)) { 845 continue; 846 } 847 $fkIndex = $index . ':fk_' . $removedForeignKey->getName(); 848 $changedTables[$fkIndex] = GeneralUtility::makeInstance( 849 TableDiff::class, 850 $changedTable->name, 851 [], 852 [], 853 [], 854 [], 855 [], 856 [], 857 $fromTable 858 ); 859 $changedTables[$fkIndex]->removedForeignKeys = [$this->buildQuotedForeignKey($removedForeignKey)]; 860 if ($isSqlite) { 861 break; 862 } 863 } 864 } 865 } 866 867 // Build a new schema diff that only contains removable fields 868 $removedFieldDiff = GeneralUtility::makeInstance( 869 SchemaDiff::class, 870 [], 871 $changedTables, 872 [], 873 $schemaDiff->fromSchema 874 ); 875 876 $statements = $removedFieldDiff->toSql($this->connection->getDatabasePlatform()); 877 878 return ['drop' => $this->calculateUpdateSuggestionsHashes($statements)]; 879 } 880 881 /** 882 * Extract update suggestions (SQL statements) for tables that can 883 * be removed from the complete schema diff. 884 * Tables that can be removed have been prefixed in a previous run 885 * of the schema migration. 886 * 887 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 888 * @return array 889 * @throws \Doctrine\DBAL\Schema\SchemaException 890 * @throws \InvalidArgumentException 891 */ 892 protected function getDropTableUpdateSuggestions(SchemaDiff $schemaDiff): array 893 { 894 $updateSuggestions = []; 895 foreach ($schemaDiff->removedTables as $removedTable) { 896 // Build a new schema diff that only contains this table 897 $tableDiff = GeneralUtility::makeInstance( 898 SchemaDiff::class, 899 [], 900 [], 901 [$this->buildQuotedTable($removedTable)], 902 $schemaDiff->fromSchema 903 ); 904 905 $statements = $tableDiff->toSql($this->connection->getDatabasePlatform()); 906 foreach ($statements as $statement) { 907 $updateSuggestions['drop_table'][md5($statement)] = $statement; 908 } 909 910 // Only store the record count for this table for the first statement, 911 // assuming that this is the actual DROP TABLE statement. 912 $updateSuggestions['tables_count'][md5($statements[0])] = $this->getTableRecordCount( 913 $removedTable->getName() 914 ); 915 } 916 917 return $updateSuggestions; 918 } 919 920 /** 921 * Move tables to be removed that are not prefixed with the deleted prefix to the list 922 * of changed tables and set a new prefixed name. 923 * Without this help the Doctrine SchemaDiff has no idea if a table has been renamed and 924 * performs a drop of the old table and creates a new table, which leads to all data in 925 * the old table being lost. 926 * 927 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 928 * @return \Doctrine\DBAL\Schema\SchemaDiff 929 * @throws \InvalidArgumentException 930 */ 931 protected function migrateUnprefixedRemovedTablesToRenames(SchemaDiff $schemaDiff): SchemaDiff 932 { 933 foreach ($schemaDiff->removedTables as $index => $removedTable) { 934 if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) { 935 continue; 936 } 937 $tableDiff = GeneralUtility::makeInstance( 938 TableDiff::class, 939 $removedTable->getQuotedName($this->connection->getDatabasePlatform()), 940 [], // added columns 941 [], // changed columns 942 [], // removed columns 943 [], // added indexes 944 [], // changed indexes 945 [], // removed indexed 946 $this->buildQuotedTable($removedTable) 947 ); 948 949 $tableDiff->newName = $this->connection->getDatabasePlatform()->quoteIdentifier( 950 substr( 951 $this->deletedPrefix . $removedTable->getName(), 952 0, 953 PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform()) 954 ) 955 ); 956 $schemaDiff->changedTables[$index] = $tableDiff; 957 unset($schemaDiff->removedTables[$index]); 958 } 959 960 return $schemaDiff; 961 } 962 963 /** 964 * Scan the list of changed tables for fields that are going to be dropped. If 965 * the name of the field does not start with the deleted prefix mark the column 966 * for a rename instead of a drop operation. 967 * 968 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 969 * @return \Doctrine\DBAL\Schema\SchemaDiff 970 * @throws \InvalidArgumentException 971 */ 972 protected function migrateUnprefixedRemovedFieldsToRenames(SchemaDiff $schemaDiff): SchemaDiff 973 { 974 foreach ($schemaDiff->changedTables as $tableIndex => $changedTable) { 975 if (count($changedTable->removedColumns) === 0) { 976 continue; 977 } 978 979 foreach ($changedTable->removedColumns as $columnIndex => $removedColumn) { 980 if (strpos($removedColumn->getName(), $this->deletedPrefix) === 0) { 981 continue; 982 } 983 984 // Build a new column object with the same properties as the removed column 985 $renamedColumnName = substr( 986 $this->deletedPrefix . $removedColumn->getName(), 987 0, 988 PlatformInformation::getMaxIdentifierLength($this->connection->getDatabasePlatform()) 989 ); 990 $renamedColumn = new Column( 991 $this->connection->quoteIdentifier($renamedColumnName), 992 $removedColumn->getType(), 993 array_diff_key($removedColumn->toArray(), ['name', 'type']) 994 ); 995 996 // Build the diff object for the column to rename 997 $columnDiff = GeneralUtility::makeInstance( 998 ColumnDiff::class, 999 $removedColumn->getQuotedName($this->connection->getDatabasePlatform()), 1000 $renamedColumn, 1001 [], // changed properties 1002 $this->buildQuotedColumn($removedColumn) 1003 ); 1004 1005 // Add the column with the required rename information to the changed column list 1006 $schemaDiff->changedTables[$tableIndex]->changedColumns[$columnIndex] = $columnDiff; 1007 1008 // Remove the column from the list of columns to be dropped 1009 unset($schemaDiff->changedTables[$tableIndex]->removedColumns[$columnIndex]); 1010 } 1011 } 1012 1013 return $schemaDiff; 1014 } 1015 1016 /** 1017 * Revert the automatic rename optimization that Doctrine performs when it detects 1018 * a column being added and a column being dropped that only differ by name. 1019 * 1020 * @param \Doctrine\DBAL\Schema\SchemaDiff $schemaDiff 1021 * @return SchemaDiff 1022 * @throws \Doctrine\DBAL\Schema\SchemaException 1023 * @throws \InvalidArgumentException 1024 */ 1025 protected function migrateColumnRenamesToDistinctActions(SchemaDiff $schemaDiff): SchemaDiff 1026 { 1027 foreach ($schemaDiff->changedTables as $index => $changedTable) { 1028 if (count($changedTable->renamedColumns) === 0) { 1029 continue; 1030 } 1031 1032 // Treat each renamed column with a new diff to get a dedicated 1033 // suggestion just for this single column. 1034 foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) { 1035 $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']); 1036 1037 $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance( 1038 Column::class, 1039 $renamedColumn->getName(), 1040 $renamedColumn->getType(), 1041 $columnOptions 1042 ); 1043 $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance( 1044 Column::class, 1045 $originalColumnName, 1046 $renamedColumn->getType(), 1047 $columnOptions 1048 ); 1049 1050 unset($changedTable->renamedColumns[$originalColumnName]); 1051 } 1052 } 1053 1054 return $schemaDiff; 1055 } 1056 1057 /** 1058 * Return the amount of records in the given table. 1059 * 1060 * @param string $tableName 1061 * @return int 1062 * @throws \InvalidArgumentException 1063 */ 1064 protected function getTableRecordCount(string $tableName): int 1065 { 1066 return GeneralUtility::makeInstance(ConnectionPool::class) 1067 ->getConnectionForTable($tableName) 1068 ->count('*', $tableName, []); 1069 } 1070 1071 /** 1072 * Determine the connection name for a table 1073 * 1074 * @param string $tableName 1075 * @return string 1076 * @throws \InvalidArgumentException 1077 */ 1078 protected function getConnectionNameForTable(string $tableName): string 1079 { 1080 $connectionNames = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames(); 1081 1082 if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) { 1083 return in_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName], $connectionNames, true) 1084 ? $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName] 1085 : ConnectionPool::DEFAULT_CONNECTION_NAME; 1086 } 1087 1088 return ConnectionPool::DEFAULT_CONNECTION_NAME; 1089 } 1090 1091 /** 1092 * Replace the array keys with a md5 sum of the actual SQL statement 1093 * 1094 * @param string[] $statements 1095 * @return string[] 1096 */ 1097 protected function calculateUpdateSuggestionsHashes(array $statements): array 1098 { 1099 return array_combine(array_map('md5', $statements), $statements); 1100 } 1101 1102 /** 1103 * Helper for buildSchemaDiff to filter an array of TableDiffs against a list of valid table names. 1104 * 1105 * @param \Doctrine\DBAL\Schema\TableDiff[]|Table[] $tableDiffs 1106 * @param string[] $validTableNames 1107 * @return \Doctrine\DBAL\Schema\TableDiff[] 1108 * @throws \InvalidArgumentException 1109 */ 1110 protected function removeUnrelatedTables(array $tableDiffs, array $validTableNames): array 1111 { 1112 return array_filter( 1113 $tableDiffs, 1114 function ($table) use ($validTableNames) { 1115 if ($table instanceof Table) { 1116 $tableName = $table->getName(); 1117 } else { 1118 $tableName = $table->newName ?: $table->name; 1119 } 1120 1121 // If the tablename has a deleted prefix strip it of before comparing 1122 // it against the list of valid table names so that drop operations 1123 // don't get removed. 1124 if (strpos($tableName, $this->deletedPrefix) === 0) { 1125 $tableName = substr($tableName, strlen($this->deletedPrefix)); 1126 } 1127 return in_array($tableName, $validTableNames, true) 1128 || in_array($this->deletedPrefix . $tableName, $validTableNames, true); 1129 } 1130 ); 1131 } 1132 1133 /** 1134 * Transform the table information to conform to specific 1135 * requirements of different database platforms like removing 1136 * the index substring length for Non-MySQL Platforms. 1137 * 1138 * @param Table[] $tables 1139 * @param \TYPO3\CMS\Core\Database\Connection $connection 1140 * @return Table[] 1141 * @throws \InvalidArgumentException 1142 */ 1143 protected function transformTablesForDatabasePlatform(array $tables, Connection $connection): array 1144 { 1145 $defaultTableOptions = $connection->getParams()['tableoptions'] ?? []; 1146 foreach ($tables as &$table) { 1147 $indexes = []; 1148 foreach ($table->getIndexes() as $key => $index) { 1149 $indexName = $index->getName(); 1150 // PostgreSQL and sqlite require index names to be unique per database/schema. 1151 if ($connection->getDatabasePlatform() instanceof PostgreSqlPlatform 1152 || $connection->getDatabasePlatform() instanceof SqlitePlatform 1153 ) { 1154 $indexName = $indexName . '_' . hash('crc32b', $table->getName() . '_' . $indexName); 1155 } 1156 1157 // Remove the length information from column names for indexes if required. 1158 $cleanedColumnNames = array_map( 1159 static function (string $columnName) use ($connection) { 1160 if ($connection->getDatabasePlatform() instanceof MySqlPlatform) { 1161 // Returning the unquoted, unmodified version of the column name since 1162 // it can include the length information for BLOB/TEXT columns which 1163 // may not be quoted. 1164 return $columnName; 1165 } 1166 1167 return $connection->quoteIdentifier(preg_replace('/\(\d+\)$/', '', $columnName)); 1168 }, 1169 $index->getUnquotedColumns() 1170 ); 1171 1172 $indexes[$key] = GeneralUtility::makeInstance( 1173 Index::class, 1174 $connection->quoteIdentifier($indexName), 1175 $cleanedColumnNames, 1176 $index->isUnique(), 1177 $index->isPrimary(), 1178 $index->getFlags(), 1179 $index->getOptions() 1180 ); 1181 } 1182 1183 $table = new Table( 1184 $table->getQuotedName($connection->getDatabasePlatform()), 1185 $table->getColumns(), 1186 $indexes, 1187 $table->getForeignKeys(), 1188 0, 1189 array_merge($defaultTableOptions, $table->getOptions()) 1190 ); 1191 } 1192 1193 return $tables; 1194 } 1195 1196 /** 1197 * Get COLLATION, ROW_FORMAT, COMMENT and ENGINE table options on MySQL connections. 1198 * 1199 * @param string[] $tableNames 1200 * @return array[] 1201 * @throws \InvalidArgumentException 1202 */ 1203 protected function getTableOptions(array $tableNames): array 1204 { 1205 $tableOptions = []; 1206 if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) { 1207 foreach ($tableNames as $tableName) { 1208 $tableOptions[$tableName] = []; 1209 } 1210 1211 return $tableOptions; 1212 } 1213 1214 $queryBuilder = $this->connection->createQueryBuilder(); 1215 $result = $queryBuilder 1216 ->select( 1217 'tables.TABLE_NAME AS table', 1218 'tables.ENGINE AS engine', 1219 'tables.ROW_FORMAT AS row_format', 1220 'tables.TABLE_COLLATION AS collate', 1221 'tables.TABLE_COMMENT AS comment', 1222 'CCSA.character_set_name AS charset' 1223 ) 1224 ->from('information_schema.TABLES', 'tables') 1225 ->join( 1226 'tables', 1227 'information_schema.COLLATION_CHARACTER_SET_APPLICABILITY', 1228 'CCSA', 1229 $queryBuilder->expr()->eq( 1230 'CCSA.collation_name', 1231 $queryBuilder->quoteIdentifier('tables.table_collation') 1232 ) 1233 ) 1234 ->where( 1235 $queryBuilder->expr()->eq( 1236 'TABLE_TYPE', 1237 $queryBuilder->createNamedParameter('BASE TABLE', \PDO::PARAM_STR) 1238 ), 1239 $queryBuilder->expr()->eq( 1240 'TABLE_SCHEMA', 1241 $queryBuilder->createNamedParameter($this->connection->getDatabase(), \PDO::PARAM_STR) 1242 ) 1243 ) 1244 ->executeQuery(); 1245 1246 while ($row = $result->fetchAssociative()) { 1247 $index = $row['table']; 1248 unset($row['table']); 1249 $tableOptions[$index] = $row; 1250 } 1251 1252 return $tableOptions; 1253 } 1254 1255 /** 1256 * Helper function to build a table object that has the _quoted attribute set so that the SchemaManager 1257 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't 1258 * provide a method to set the flag after the object has been instantiated and there's no possibility to 1259 * hook into the createSchema() method early enough to influence the original table object. 1260 * 1261 * @param \Doctrine\DBAL\Schema\Table $table 1262 * @return \Doctrine\DBAL\Schema\Table 1263 */ 1264 protected function buildQuotedTable(Table $table): Table 1265 { 1266 $databasePlatform = $this->connection->getDatabasePlatform(); 1267 1268 return new Table( 1269 $databasePlatform->quoteIdentifier($table->getName()), 1270 $table->getColumns(), 1271 $table->getIndexes(), 1272 $table->getForeignKeys(), 1273 0, 1274 $table->getOptions() 1275 ); 1276 } 1277 1278 /** 1279 * Helper function to build a column object that has the _quoted attribute set so that the SchemaManager 1280 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't 1281 * provide a method to set the flag after the object has been instantiated and there's no possibility to 1282 * hook into the createSchema() method early enough to influence the original column object. 1283 * 1284 * @param \Doctrine\DBAL\Schema\Column $column 1285 * @return \Doctrine\DBAL\Schema\Column 1286 */ 1287 protected function buildQuotedColumn(Column $column): Column 1288 { 1289 $databasePlatform = $this->connection->getDatabasePlatform(); 1290 1291 return GeneralUtility::makeInstance( 1292 Column::class, 1293 $databasePlatform->quoteIdentifier($column->getName()), 1294 $column->getType(), 1295 array_diff_key($column->toArray(), ['name', 'type']) 1296 ); 1297 } 1298 1299 /** 1300 * Helper function to build an index object that has the _quoted attribute set so that the SchemaManager 1301 * will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine doesn't 1302 * provide a method to set the flag after the object has been instantiated and there's no possibility to 1303 * hook into the createSchema() method early enough to influence the original column object. 1304 * 1305 * @param \Doctrine\DBAL\Schema\Index $index 1306 * @return \Doctrine\DBAL\Schema\Index 1307 */ 1308 protected function buildQuotedIndex(Index $index): Index 1309 { 1310 $databasePlatform = $this->connection->getDatabasePlatform(); 1311 1312 return GeneralUtility::makeInstance( 1313 Index::class, 1314 $databasePlatform->quoteIdentifier($index->getName()), 1315 $index->getColumns(), 1316 $index->isUnique(), 1317 $index->isPrimary(), 1318 $index->getFlags(), 1319 $index->getOptions() 1320 ); 1321 } 1322 1323 /** 1324 * Helper function to build a foreign key constraint object that has the _quoted attribute set so that the 1325 * SchemaManager will use quoted identifiers when creating the final SQL statements. This is needed as Doctrine 1326 * doesn't provide a method to set the flag after the object has been instantiated and there's no possibility to 1327 * hook into the createSchema() method early enough to influence the original column object. 1328 * 1329 * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $index 1330 * @return \Doctrine\DBAL\Schema\ForeignKeyConstraint 1331 */ 1332 protected function buildQuotedForeignKey(ForeignKeyConstraint $index): ForeignKeyConstraint 1333 { 1334 $databasePlatform = $this->connection->getDatabasePlatform(); 1335 1336 return GeneralUtility::makeInstance( 1337 ForeignKeyConstraint::class, 1338 $index->getLocalColumns(), 1339 $databasePlatform->quoteIdentifier($index->getForeignTableName()), 1340 $index->getForeignColumns(), 1341 $databasePlatform->quoteIdentifier($index->getName()), 1342 $index->getOptions() 1343 ); 1344 } 1345 1346 protected function getDatabasePlatform(string $tableName): string 1347 { 1348 $databasePlatform = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->getDatabasePlatform(); 1349 if ($databasePlatform instanceof PostgreSqlPlatform) { 1350 return 'postgresql'; 1351 } 1352 if ($databasePlatform instanceof SQLServerPlatform) { 1353 return 'mssql'; 1354 } 1355 if ($databasePlatform instanceof SqlitePlatform) { 1356 return 'sqlite'; 1357 } 1358 1359 return 'mysql'; 1360 } 1361} 1362