Add a mechanism to try the database migration on a copy of the tables before running it on the "real" data

This commit is contained in:
Robin Appelman 2014-01-29 15:48:12 +01:00
parent b62cb00771
commit 9c6a93a87c
3 changed files with 242 additions and 0 deletions

View File

@ -0,0 +1,19 @@
<?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);
}
}

139
lib/private/db/migrator.php Normal file
View File

@ -0,0 +1,139 @@
<?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\Table;
use \Doctrine\DBAL\Schema\Schema;
use \Doctrine\DBAL\Schema\SchemaConfig;
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 Schema $targetSchema
*/
public function checkMigrate(Schema $targetSchema) {
/**
* @var \Doctrine\DBAL\Schema\Table[] $tables
*/
$tables = $targetSchema->getTables();
$existingTables = $this->connection->getSchemaManager()->listTableNames();
foreach ($tables as $table) {
// don't need to check for new tables
list(, $tableName) = explode('.', $table->getName());
if (array_search($tableName, $existingTables) !== false) {
$this->checkTableMigrate($table);
}
}
}
/**
* 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
*/
protected function checkTableMigrate(Table $table) {
$name = $table->getName();
$tmpName = $name . '_copy_' . uniqid();
$this->copyTable($name, $tmpName);
//create the migration schema for the temporary table
$tmpTable = new Table($tmpName, $table->getColumns(), $table->getIndexes(), $table->getForeignKeys(), $table->_idGeneratorType, $table->getOptions());
$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) {
$this->dropTable($tmpName);
throw new MigrationException($table->getName(), $e->getMessage());
}
}
/**
* @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;
}
$sourceSchema = $this->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 \Doctrine\DBAL\Schema\Comparator();
$schemaDiff = $comparator->compare($sourceSchema, $targetSchema);
foreach ($schemaDiff->changedTables as $tableDiff) {
$tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name);
}
$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));
}
}

84
tests/lib/db/migrator.php Normal file
View File

@ -0,0 +1,84 @@
<?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 Test\DB;
use \Doctrine\DBAL\Schema\Schema;
use \Doctrine\DBAL\Schema\SchemaConfig;
class Migrator extends \PHPUnit_Framework_TestCase {
/**
* @var \Doctrine\DBAL\Connection $connection
*/
private $connection;
private $tableName;
public function setUp() {
$this->tableName = 'test_' . uniqid();
$this->connection = \OC_DB::getConnection();
$this->tableName = $this->connection->getDatabase() . '.' . $this->tableName;
}
public function tearDown() {
$this->connection->exec('DROP TABLE ' . $this->tableName);
}
private function getInitialSchema() {
$schema = new Schema(array(), array(), $this->getSchemaConfig());
$table = $schema->createTable($this->tableName);
$table->addColumn('id', 'integer');
$table->addColumn('name', 'string');
return $schema;
}
private function getNewSchema() {
$schema = new Schema(array(), array(), $this->getSchemaConfig());
$table = $schema->createTable($this->tableName);
$table->addColumn('id', 'integer');
$table->addColumn('name', 'string');
$table->addUniqueIndex(array('id'));
return $schema;
}
private function getSchemaConfig() {
$config = new SchemaConfig();
$config->setName($this->connection->getDatabase());
return $config;
}
/**
* @expectedException \OC\DB\MigrationException
*/
public function testDuplicateKeyUpgrade() {
$migrator = new \OC\DB\Migrator($this->connection);
$migrator->migrate($this->getInitialSchema());
$this->connection->insert($this->tableName, array('id' => 1, 'name' => 'foo'));
$this->connection->insert($this->tableName, array('id' => 2, 'name' => 'bar'));
$this->connection->insert($this->tableName, array('id' => 2, 'name' => 'qwerty'));
$migrator->checkMigrate($this->getNewSchema());
$this->fail('checkMigrate should have failed');
}
public function testUpgrade() {
$migrator = new \OC\DB\Migrator($this->connection);
$migrator->migrate($this->getInitialSchema());
$this->connection->insert($this->tableName, array('id' => 1, 'name' => 'foo'));
$this->connection->insert($this->tableName, array('id' => 2, 'name' => 'bar'));
$this->connection->insert($this->tableName, array('id' => 3, 'name' => 'qwerty'));
$newSchema = $this->getNewSchema();
$migrator->checkMigrate($newSchema);
$migrator->migrate($newSchema);
$this->assertTrue(true);
}
}