Migrator.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\DB;
  8. use Doctrine\DBAL\Connection;
  9. use Doctrine\DBAL\Exception;
  10. use Doctrine\DBAL\Platforms\MySQLPlatform;
  11. use Doctrine\DBAL\Schema\AbstractAsset;
  12. use Doctrine\DBAL\Schema\Schema;
  13. use Doctrine\DBAL\Schema\SchemaDiff;
  14. use Doctrine\DBAL\Types\StringType;
  15. use Doctrine\DBAL\Types\Type;
  16. use OCP\EventDispatcher\IEventDispatcher;
  17. use OCP\IConfig;
  18. use function preg_match;
  19. class Migrator {
  20. /** @var Connection */
  21. protected $connection;
  22. /** @var IConfig */
  23. protected $config;
  24. private ?IEventDispatcher $dispatcher;
  25. /** @var bool */
  26. private $noEmit = false;
  27. public function __construct(Connection $connection,
  28. IConfig $config,
  29. ?IEventDispatcher $dispatcher = null) {
  30. $this->connection = $connection;
  31. $this->config = $config;
  32. $this->dispatcher = $dispatcher;
  33. }
  34. /**
  35. * @throws Exception
  36. */
  37. public function migrate(Schema $targetSchema) {
  38. $this->noEmit = true;
  39. $this->applySchema($targetSchema);
  40. }
  41. /**
  42. * @return string
  43. */
  44. public function generateChangeScript(Schema $targetSchema) {
  45. $schemaDiff = $this->getDiff($targetSchema, $this->connection);
  46. $script = '';
  47. $sqls = $this->connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff);
  48. foreach ($sqls as $sql) {
  49. $script .= $this->convertStatementToScript($sql);
  50. }
  51. return $script;
  52. }
  53. /**
  54. * @throws Exception
  55. */
  56. public function createSchema() {
  57. $this->connection->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
  58. /** @var string|AbstractAsset $asset */
  59. $filterExpression = $this->getFilterExpression();
  60. if ($asset instanceof AbstractAsset) {
  61. return preg_match($filterExpression, $asset->getName()) === 1;
  62. }
  63. return preg_match($filterExpression, $asset) === 1;
  64. });
  65. return $this->connection->createSchemaManager()->introspectSchema();
  66. }
  67. /**
  68. * @return SchemaDiff
  69. */
  70. protected function getDiff(Schema $targetSchema, Connection $connection) {
  71. // Adjust STRING columns with a length higher than 4000 to TEXT (clob)
  72. // for consistency between the supported databases and
  73. // old vs. new installations.
  74. foreach ($targetSchema->getTables() as $table) {
  75. foreach ($table->getColumns() as $column) {
  76. if ($column->getType() instanceof StringType) {
  77. if ($column->getLength() > 4000) {
  78. $column->setType(Type::getType('text'));
  79. $column->setLength(null);
  80. }
  81. }
  82. }
  83. }
  84. $this->connection->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
  85. /** @var string|AbstractAsset $asset */
  86. $filterExpression = $this->getFilterExpression();
  87. if ($asset instanceof AbstractAsset) {
  88. return preg_match($filterExpression, $asset->getName()) === 1;
  89. }
  90. return preg_match($filterExpression, $asset) === 1;
  91. });
  92. $sourceSchema = $connection->createSchemaManager()->introspectSchema();
  93. // remove tables we don't know about
  94. foreach ($sourceSchema->getTables() as $table) {
  95. if (!$targetSchema->hasTable($table->getName())) {
  96. $sourceSchema->dropTable($table->getName());
  97. }
  98. }
  99. // remove sequences we don't know about
  100. foreach ($sourceSchema->getSequences() as $table) {
  101. if (!$targetSchema->hasSequence($table->getName())) {
  102. $sourceSchema->dropSequence($table->getName());
  103. }
  104. }
  105. $comparator = $connection->createSchemaManager()->createComparator();
  106. return $comparator->compareSchemas($sourceSchema, $targetSchema);
  107. }
  108. /**
  109. * @throws Exception
  110. */
  111. protected function applySchema(Schema $targetSchema, ?Connection $connection = null) {
  112. if (is_null($connection)) {
  113. $connection = $this->connection;
  114. }
  115. $schemaDiff = $this->getDiff($targetSchema, $connection);
  116. if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) {
  117. $connection->beginTransaction();
  118. }
  119. $sqls = $connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff);
  120. $step = 0;
  121. foreach ($sqls as $sql) {
  122. $this->emit($sql, $step++, count($sqls));
  123. $connection->executeStatement($sql);
  124. }
  125. if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) {
  126. $connection->commit();
  127. }
  128. }
  129. /**
  130. * @param $statement
  131. * @return string
  132. */
  133. protected function convertStatementToScript($statement) {
  134. $script = $statement . ';';
  135. $script .= PHP_EOL;
  136. $script .= PHP_EOL;
  137. return $script;
  138. }
  139. protected function getFilterExpression() {
  140. return '/^' . preg_quote($this->config->getSystemValueString('dbtableprefix', 'oc_'), '/') . '/';
  141. }
  142. protected function emit(string $sql, int $step, int $max): void {
  143. if ($this->noEmit) {
  144. return;
  145. }
  146. if (is_null($this->dispatcher)) {
  147. return;
  148. }
  149. $this->dispatcher->dispatchTyped(new MigratorExecuteSqlEvent($sql, $step, $max));
  150. }
  151. }