migrator.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. /**
  3. * @author martin-rueegg <martin.rueegg@metaworx.ch>
  4. * @author Morris Jobke <hey@morrisjobke.de>
  5. * @author Robin Appelman <icewind@owncloud.com>
  6. * @author tbelau666 <thomas.belau@gmx.de>
  7. * @author Thomas Müller <thomas.mueller@tmit.eu>
  8. * @author Vincent Petry <pvince81@owncloud.com>
  9. *
  10. * @copyright Copyright (c) 2015, ownCloud, Inc.
  11. * @license AGPL-3.0
  12. *
  13. * This code is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License, version 3,
  15. * as published by the Free Software Foundation.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License, version 3,
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>
  24. *
  25. */
  26. namespace OC\DB;
  27. use \Doctrine\DBAL\DBALException;
  28. use \Doctrine\DBAL\Schema\Index;
  29. use \Doctrine\DBAL\Schema\Table;
  30. use \Doctrine\DBAL\Schema\Schema;
  31. use \Doctrine\DBAL\Schema\SchemaConfig;
  32. use \Doctrine\DBAL\Schema\Comparator;
  33. use OCP\IConfig;
  34. use OCP\Security\ISecureRandom;
  35. class Migrator {
  36. /**
  37. * @var \Doctrine\DBAL\Connection $connection
  38. */
  39. protected $connection;
  40. /**
  41. * @var ISecureRandom
  42. */
  43. private $random;
  44. /** @var IConfig */
  45. protected $config;
  46. /**
  47. * @param \Doctrine\DBAL\Connection $connection
  48. * @param ISecureRandom $random
  49. * @param IConfig $config
  50. */
  51. public function __construct(\Doctrine\DBAL\Connection $connection, ISecureRandom $random, IConfig $config) {
  52. $this->connection = $connection;
  53. $this->random = $random;
  54. $this->config = $config;
  55. }
  56. /**
  57. * @param \Doctrine\DBAL\Schema\Schema $targetSchema
  58. */
  59. public function migrate(Schema $targetSchema) {
  60. $this->applySchema($targetSchema);
  61. }
  62. /**
  63. * @param \Doctrine\DBAL\Schema\Schema $targetSchema
  64. * @return string
  65. */
  66. public function generateChangeScript(Schema $targetSchema) {
  67. $schemaDiff = $this->getDiff($targetSchema, $this->connection);
  68. $script = '';
  69. $sqls = $schemaDiff->toSql($this->connection->getDatabasePlatform());
  70. foreach ($sqls as $sql) {
  71. $script .= $this->convertStatementToScript($sql);
  72. }
  73. return $script;
  74. }
  75. /**
  76. * @param Schema $targetSchema
  77. * @throws \OC\DB\MigrationException
  78. */
  79. public function checkMigrate(Schema $targetSchema) {
  80. /**
  81. * @var \Doctrine\DBAL\Schema\Table[] $tables
  82. */
  83. $tables = $targetSchema->getTables();
  84. $filterExpression = $this->getFilterExpression();
  85. $this->connection->getConfiguration()->
  86. setFilterSchemaAssetsExpression($filterExpression);
  87. $existingTables = $this->connection->getSchemaManager()->listTableNames();
  88. foreach ($tables as $table) {
  89. if (strpos($table->getName(), '.')) {
  90. list(, $tableName) = explode('.', $table->getName());
  91. } else {
  92. $tableName = $table->getName();
  93. }
  94. // don't need to check for new tables
  95. if (array_search($tableName, $existingTables) !== false) {
  96. $this->checkTableMigrate($table);
  97. }
  98. }
  99. }
  100. /**
  101. * Create a unique name for the temporary table
  102. *
  103. * @param string $name
  104. * @return string
  105. */
  106. protected function generateTemporaryTableName($name) {
  107. return $this->config->getSystemValue('dbtableprefix', 'oc_') . $name . '_' . $this->random->generate(13, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
  108. }
  109. /**
  110. * Check the migration of a table on a copy so we can detect errors before messing with the real table
  111. *
  112. * @param \Doctrine\DBAL\Schema\Table $table
  113. * @throws \OC\DB\MigrationException
  114. */
  115. protected function checkTableMigrate(Table $table) {
  116. $name = $table->getName();
  117. $tmpName = $this->generateTemporaryTableName($name);
  118. $this->copyTable($name, $tmpName);
  119. //create the migration schema for the temporary table
  120. $tmpTable = $this->renameTableSchema($table, $tmpName);
  121. $schemaConfig = new SchemaConfig();
  122. $schemaConfig->setName($this->connection->getDatabase());
  123. $schema = new Schema(array($tmpTable), array(), $schemaConfig);
  124. try {
  125. $this->applySchema($schema);
  126. $this->dropTable($tmpName);
  127. } catch (DBALException $e) {
  128. // pgsql needs to commit it's failed transaction before doing anything else
  129. if ($this->connection->isTransactionActive()) {
  130. $this->connection->commit();
  131. }
  132. $this->dropTable($tmpName);
  133. throw new MigrationException($table->getName(), $e->getMessage());
  134. }
  135. }
  136. /**
  137. * @param \Doctrine\DBAL\Schema\Table $table
  138. * @param string $newName
  139. * @return \Doctrine\DBAL\Schema\Table
  140. */
  141. protected function renameTableSchema(Table $table, $newName) {
  142. /**
  143. * @var \Doctrine\DBAL\Schema\Index[] $indexes
  144. */
  145. $indexes = $table->getIndexes();
  146. $newIndexes = array();
  147. foreach ($indexes as $index) {
  148. if ($index->isPrimary()) {
  149. // do not rename primary key
  150. $indexName = $index->getName();
  151. } else {
  152. // avoid conflicts in index names
  153. $indexName = $this->config->getSystemValue('dbtableprefix', 'oc_') . $this->random->generate(13, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
  154. }
  155. $newIndexes[] = new Index($indexName, $index->getColumns(), $index->isUnique(), $index->isPrimary());
  156. }
  157. // foreign keys are not supported so we just set it to an empty array
  158. return new Table($newName, $table->getColumns(), $newIndexes, array(), 0, $table->getOptions());
  159. }
  160. protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) {
  161. $filterExpression = $this->getFilterExpression();
  162. $this->connection->getConfiguration()->
  163. setFilterSchemaAssetsExpression($filterExpression);
  164. $sourceSchema = $connection->getSchemaManager()->createSchema();
  165. // remove tables we don't know about
  166. /** @var $table \Doctrine\DBAL\Schema\Table */
  167. foreach ($sourceSchema->getTables() as $table) {
  168. if (!$targetSchema->hasTable($table->getName())) {
  169. $sourceSchema->dropTable($table->getName());
  170. }
  171. }
  172. // remove sequences we don't know about
  173. foreach ($sourceSchema->getSequences() as $table) {
  174. if (!$targetSchema->hasSequence($table->getName())) {
  175. $sourceSchema->dropSequence($table->getName());
  176. }
  177. }
  178. $comparator = new Comparator();
  179. return $comparator->compare($sourceSchema, $targetSchema);
  180. }
  181. /**
  182. * @param \Doctrine\DBAL\Schema\Schema $targetSchema
  183. * @param \Doctrine\DBAL\Connection $connection
  184. */
  185. protected function applySchema(Schema $targetSchema, \Doctrine\DBAL\Connection $connection = null) {
  186. if (is_null($connection)) {
  187. $connection = $this->connection;
  188. }
  189. $schemaDiff = $this->getDiff($targetSchema, $connection);
  190. $connection->beginTransaction();
  191. foreach ($schemaDiff->toSql($connection->getDatabasePlatform()) as $sql) {
  192. $connection->query($sql);
  193. }
  194. $connection->commit();
  195. }
  196. /**
  197. * @param string $sourceName
  198. * @param string $targetName
  199. */
  200. protected function copyTable($sourceName, $targetName) {
  201. $quotedSource = $this->connection->quoteIdentifier($sourceName);
  202. $quotedTarget = $this->connection->quoteIdentifier($targetName);
  203. $this->connection->exec('CREATE TABLE ' . $quotedTarget . ' (LIKE ' . $quotedSource . ')');
  204. $this->connection->exec('INSERT INTO ' . $quotedTarget . ' SELECT * FROM ' . $quotedSource);
  205. }
  206. /**
  207. * @param string $name
  208. */
  209. protected function dropTable($name) {
  210. $this->connection->exec('DROP TABLE ' . $this->connection->quoteIdentifier($name));
  211. }
  212. /**
  213. * @param $statement
  214. * @return string
  215. */
  216. protected function convertStatementToScript($statement) {
  217. $script = $statement . ';';
  218. $script .= PHP_EOL;
  219. $script .= PHP_EOL;
  220. return $script;
  221. }
  222. protected function getFilterExpression() {
  223. return '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
  224. }
  225. }