diff options
author | icewind1991 <robin@icewind.nl> | 2014-06-03 16:27:06 +0200 |
---|---|---|
committer | icewind1991 <robin@icewind.nl> | 2014-06-03 16:27:06 +0200 |
commit | 68b15f3282fa0ef8c0f4e3a4e659a09eab02ef46 (patch) | |
tree | 811b9057d2a131b7a8d72e2bad2494ac230b0fef /lib/private/db | |
parent | a5dd8e84f13c9815c0208851664238cfc0034b5b (diff) | |
parent | 4429b54ce4fdf5aa2936dd733f5c1751d342177e (diff) | |
download | nextcloud-server-68b15f3282fa0ef8c0f4e3a4e659a09eab02ef46.tar.gz nextcloud-server-68b15f3282fa0ef8c0f4e3a4e659a09eab02ef46.zip |
Merge pull request #7015 from owncloud/db-migration-check-copy
Check database migration on a copy of the table first
Diffstat (limited to 'lib/private/db')
-rw-r--r-- | lib/private/db/mdb2schemamanager.php | 95 | ||||
-rw-r--r-- | lib/private/db/migrationexception.php | 26 | ||||
-rw-r--r-- | lib/private/db/migrator.php | 196 | ||||
-rw-r--r-- | lib/private/db/mysqlmigrator.php | 32 | ||||
-rw-r--r-- | lib/private/db/nocheckmigrator.php | 24 | ||||
-rw-r--r-- | lib/private/db/oraclemigrator.php | 40 | ||||
-rw-r--r-- | lib/private/db/sqlitemigrator.php | 40 |
7 files changed, 403 insertions, 50 deletions
diff --git a/lib/private/db/mdb2schemamanager.php b/lib/private/db/mdb2schemamanager.php index 4208dbd18f4..533ed9110e7 100644 --- a/lib/private/db/mdb2schemamanager.php +++ b/lib/private/db/mdb2schemamanager.php @@ -8,6 +8,11 @@ namespace OC\DB; +use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\PostgreSqlPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; + class MDB2SchemaManager { /** * @var \OC\DB\Connection $conn @@ -31,7 +36,7 @@ class MDB2SchemaManager { * * TODO: write more documentation */ - public function getDbStructure( $file, $mode = MDB2_SCHEMA_DUMP_STRUCTURE) { + public function getDbStructure($file, $mode = MDB2_SCHEMA_DUMP_STRUCTURE) { $sm = $this->conn->getSchemaManager(); return \OC_DB_MDB2SchemaWriter::saveSchemaToFile($file, $sm); @@ -44,58 +49,64 @@ class MDB2SchemaManager { * * TODO: write more documentation */ - public function createDbFromStructure( $file ) { + public function createDbFromStructure($file) { $schemaReader = new MDB2SchemaReader(\OC_Config::getObject(), $this->conn->getDatabasePlatform()); $toSchema = $schemaReader->loadSchemaFromFile($file); return $this->executeSchemaChange($toSchema); } /** + * @return \OC\DB\Migrator + */ + protected function getMigrator() { + $platform = $this->conn->getDatabasePlatform(); + if ($platform instanceof SqlitePlatform) { + return new SQLiteMigrator($this->conn); + } else if ($platform instanceof OraclePlatform) { + return new OracleMigrator($this->conn); + } else if ($platform instanceof MySqlPlatform) { + return new MySQLMigrator($this->conn); + } else if ($platform instanceof PostgreSqlPlatform) { + return new Migrator($this->conn); + } else { + return new NoCheckMigrator($this->conn); + } + } + + /** * update the database scheme * @param string $file file to read structure from + * @param bool $generateSql only return the sql needed for the upgrade * @return string|boolean */ public function updateDbFromStructure($file, $generateSql = false) { - $sm = $this->conn->getSchemaManager(); - $fromSchema = $sm->createSchema(); - - $schemaReader = new MDB2SchemaReader(\OC_Config::getObject(), $this->conn->getDatabasePlatform()); - $toSchema = $schemaReader->loadSchemaFromFile($file); - - // remove tables we don't know about - /** @var $table \Doctrine\DBAL\Schema\Table */ - foreach($fromSchema->getTables() as $table) { - if (!$toSchema->hasTable($table->getName())) { - $fromSchema->dropTable($table->getName()); - } - } - // remove sequences we don't know about - foreach($fromSchema->getSequences() as $table) { - if (!$toSchema->hasSequence($table->getName())) { - $fromSchema->dropSequence($table->getName()); - } - } - - $comparator = new \Doctrine\DBAL\Schema\Comparator(); - $schemaDiff = $comparator->compare($fromSchema, $toSchema); $platform = $this->conn->getDatabasePlatform(); - foreach($schemaDiff->changedTables as $tableDiff) { - $tableDiff->name = $platform->quoteIdentifier($tableDiff->name); - foreach($tableDiff->changedColumns as $column) { - $column->oldColumnName = $platform->quoteIdentifier($column->oldColumnName); - } - } + $schemaReader = new MDB2SchemaReader(\OC_Config::getObject(), $platform); + $toSchema = $schemaReader->loadSchemaFromFile($file); + $migrator = $this->getMigrator(); if ($generateSql) { - return $this->generateChangeScript($schemaDiff); + return $migrator->generateChangeScript($toSchema); + } else { + $migrator->checkMigrate($toSchema); + $migrator->migrate($toSchema); + return true; } + } - return $this->executeSchemaChange($schemaDiff); + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + * @return string + */ + public function generateChangeScript($schema) { + $migrator = $this->getMigrator(); + return $migrator->generateChangeScript($schema); } /** * remove all tables defined in a database structure xml file + * * @param string $file the xml file describing the tables */ public function removeDBStructure($file) { @@ -103,7 +114,7 @@ class MDB2SchemaManager { $fromSchema = $schemaReader->loadSchemaFromFile($file); $toSchema = clone $fromSchema; /** @var $table \Doctrine\DBAL\Schema\Table */ - foreach($toSchema->getTables() as $table) { + foreach ($toSchema->getTables() as $table) { $toSchema->dropTable($table->getName()); } $comparator = new \Doctrine\DBAL\Schema\Comparator(); @@ -117,26 +128,10 @@ class MDB2SchemaManager { */ private function executeSchemaChange($schema) { $this->conn->beginTransaction(); - foreach($schema->toSql($this->conn->getDatabasePlatform()) as $sql) { + foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) { $this->conn->query($sql); } $this->conn->commit(); return true; } - - /** - * @param \Doctrine\DBAL\Schema\Schema $schema - * @return string - */ - public function generateChangeScript($schema) { - - $script = ''; - $sqls = $schema->toSql($this->conn->getDatabasePlatform()); - foreach($sqls as $sql) { - $script .= $sql . ';'; - $script .= PHP_EOL; - } - - return $script; - } } diff --git a/lib/private/db/migrationexception.php b/lib/private/db/migrationexception.php new file mode 100644 index 00000000000..2afec9700a0 --- /dev/null +++ b/lib/private/db/migrationexception.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + + +class MigrationException extends \Exception { + private $table; + + public function __construct($table, $message) { + $this->table = $table; + parent::__construct($message); + } + + /** + * @return string + */ + public function getTable() { + return $this->table; + } +} diff --git a/lib/private/db/migrator.php b/lib/private/db/migrator.php new file mode 100644 index 00000000000..517be8399e8 --- /dev/null +++ b/lib/private/db/migrator.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + +use \Doctrine\DBAL\DBALException; +use \Doctrine\DBAL\Schema\Index; +use \Doctrine\DBAL\Schema\Table; +use \Doctrine\DBAL\Schema\Schema; +use \Doctrine\DBAL\Schema\SchemaConfig; +use \Doctrine\DBAL\Schema\Comparator; + +class Migrator { + /** + * @var \Doctrine\DBAL\Connection $connection + */ + protected $connection; + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + public function __construct(\Doctrine\DBAL\Connection $connection) { + $this->connection = $connection; + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + */ + public function migrate(Schema $targetSchema) { + $this->applySchema($targetSchema); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @return string + */ + public function generateChangeScript(Schema $targetSchema) { + $schemaDiff = $this->getDiff($targetSchema, $this->connection); + + $script = ''; + $sqls = $schemaDiff->toSql($this->connection->getDatabasePlatform()); + foreach ($sqls as $sql) { + $script .= $sql . ';'; + $script .= PHP_EOL; + } + + return $script; + } + + /** + * @param Schema $targetSchema + * @throws \OC\DB\MigrationException + */ + public function checkMigrate(Schema $targetSchema) { + /** + * @var \Doctrine\DBAL\Schema\Table[] $tables + */ + $tables = $targetSchema->getTables(); + + $existingTables = $this->connection->getSchemaManager()->listTableNames(); + + foreach ($tables as $table) { + if (strpos($table->getName(), '.')) { + list(, $tableName) = explode('.', $table->getName()); + } else { + $tableName = $table->getName(); + } + // don't need to check for new tables + if (array_search($tableName, $existingTables) !== false) { + $this->checkTableMigrate($table); + } + } + } + + /** + * Create a unique name for the temporary table + * + * @param string $name + * @return string + */ + protected function generateTemporaryTableName($name) { + return 'oc_' . $name . '_' . uniqid(); + } + + /** + * Check the migration of a table on a copy so we can detect errors before messing with the real table + * + * @param \Doctrine\DBAL\Schema\Table $table + * @throws \OC\DB\MigrationException + */ + protected function checkTableMigrate(Table $table) { + $name = $table->getName(); + $tmpName = $this->generateTemporaryTableName($name); + + $this->copyTable($name, $tmpName); + + //create the migration schema for the temporary table + $tmpTable = $this->renameTableSchema($table, $tmpName); + $schemaConfig = new SchemaConfig(); + $schemaConfig->setName($this->connection->getDatabase()); + $schema = new Schema(array($tmpTable), array(), $schemaConfig); + + try { + $this->applySchema($schema); + $this->dropTable($tmpName); + } catch (DBALException $e) { + // pgsql needs to commit it's failed transaction before doing anything else + $this->connection->commit(); + $this->dropTable($tmpName); + throw new MigrationException($table->getName(), $e->getMessage()); + } + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param string $newName + * @return \Doctrine\DBAL\Schema\Table + */ + protected function renameTableSchema(Table $table, $newName) { + /** + * @var \Doctrine\DBAL\Schema\Index[] $indexes + */ + $indexes = $table->getIndexes(); + $newIndexes = array(); + foreach ($indexes as $index) { + $indexName = 'oc_' . uniqid(); // avoid conflicts in index names + $newIndexes[] = new Index($indexName, $index->getColumns(), $index->isUnique(), $index->isPrimary()); + } + + // foreign keys are not supported so we just set it to an empty array + return new Table($newName, $table->getColumns(), $newIndexes, array(), 0, $table->getOptions()); + } + + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $sourceSchema = $connection->getSchemaManager()->createSchema(); + + // remove tables we don't know about + /** @var $table \Doctrine\DBAL\Schema\Table */ + foreach ($sourceSchema->getTables() as $table) { + if (!$targetSchema->hasTable($table->getName())) { + $sourceSchema->dropTable($table->getName()); + } + } + // remove sequences we don't know about + foreach ($sourceSchema->getSequences() as $table) { + if (!$targetSchema->hasSequence($table->getName())) { + $sourceSchema->dropSequence($table->getName()); + } + } + + $comparator = new Comparator(); + return $comparator->compare($sourceSchema, $targetSchema); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + */ + protected function applySchema(Schema $targetSchema, \Doctrine\DBAL\Connection $connection = null) { + if (is_null($connection)) { + $connection = $this->connection; + } + + $schemaDiff = $this->getDiff($targetSchema, $connection); + + $connection->beginTransaction(); + foreach ($schemaDiff->toSql($connection->getDatabasePlatform()) as $sql) { + $connection->query($sql); + } + $connection->commit(); + } + + /** + * @param string $sourceName + * @param string $targetName + */ + protected function copyTable($sourceName, $targetName) { + $quotedSource = $this->connection->quoteIdentifier($sourceName); + $quotedTarget = $this->connection->quoteIdentifier($targetName); + + $this->connection->exec('CREATE TABLE ' . $quotedTarget . ' (LIKE ' . $quotedSource . ')'); + $this->connection->exec('INSERT INTO ' . $quotedTarget . ' SELECT * FROM ' . $quotedSource); + } + + /** + * @param string $name + */ + protected function dropTable($name) { + $this->connection->exec('DROP TABLE ' . $this->connection->quoteIdentifier($name)); + } +} diff --git a/lib/private/db/mysqlmigrator.php b/lib/private/db/mysqlmigrator.php new file mode 100644 index 00000000000..97495f52032 --- /dev/null +++ b/lib/private/db/mysqlmigrator.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +class MySQLMigrator extends Migrator { + /** + * @param Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + * @return \Doctrine\DBAL\Schema\SchemaDiff + */ + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $schemaDiff = parent::getDiff($targetSchema, $connection); + + // identifiers need to be quoted for mysql + foreach ($schemaDiff->changedTables as $tableDiff) { + $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); + foreach ($tableDiff->changedColumns as $column) { + $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); + } + } + + return $schemaDiff; + } +} diff --git a/lib/private/db/nocheckmigrator.php b/lib/private/db/nocheckmigrator.php new file mode 100644 index 00000000000..cd2b47c214a --- /dev/null +++ b/lib/private/db/nocheckmigrator.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +/** + * migrator for database platforms that don't support the upgrade check + * + * @package OC\DB + */ +class NoCheckMigrator extends Migrator { + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @throws \OC\DB\MigrationException + */ + public function checkMigrate(Schema $targetSchema) {} +} diff --git a/lib/private/db/oraclemigrator.php b/lib/private/db/oraclemigrator.php new file mode 100644 index 00000000000..1a8df2def9c --- /dev/null +++ b/lib/private/db/oraclemigrator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +class OracleMigrator extends NoCheckMigrator { + /** + * @param Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + * @return \Doctrine\DBAL\Schema\SchemaDiff + */ + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $schemaDiff = parent::getDiff($targetSchema, $connection); + + // oracle forces us to quote the identifiers + foreach ($schemaDiff->changedTables as $tableDiff) { + $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); + foreach ($tableDiff->changedColumns as $column) { + $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); + } + } + + return $schemaDiff; + } + + /** + * @param string $name + * @return string + */ + protected function generateTemporaryTableName($name) { + return 'oc_' . uniqid(); + } +} diff --git a/lib/private/db/sqlitemigrator.php b/lib/private/db/sqlitemigrator.php new file mode 100644 index 00000000000..f5f78986771 --- /dev/null +++ b/lib/private/db/sqlitemigrator.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\DB; + +use Doctrine\DBAL\DBALException; + +class SQLiteMigrator extends Migrator { + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @throws \OC\DB\MigrationException + * + * For sqlite we simple make a copy of the entire database, and test the migration on that + */ + public function checkMigrate(\Doctrine\DBAL\Schema\Schema $targetSchema) { + $dbFile = $this->connection->getDatabase(); + $tmpFile = \OC_Helper::tmpFile('.db'); + copy($dbFile, $tmpFile); + + $connectionParams = array( + 'path' => $tmpFile, + 'driver' => 'pdo_sqlite', + ); + $conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams); + try { + $this->applySchema($targetSchema, $conn); + $conn->close(); + unlink($tmpFile); + } catch (DBALException $e) { + $conn->close(); + unlink($tmpFile); + throw new MigrationException('', $e->getMessage()); + } + } +} |