From 9c6a93a87c835dee5fb0b580865d4f70836685cf Mon Sep 17 00:00:00 2001
From: Robin Appelman <icewind@owncloud.com>
Date: Wed, 29 Jan 2014 15:48:12 +0100
Subject: [PATCH] Add a mechanism to try the database migration on a copy of
 the tables before running it on the "real" data

---
 lib/private/db/migrationexception.php |  19 ++++
 lib/private/db/migrator.php           | 139 ++++++++++++++++++++++++++
 tests/lib/db/migrator.php             |  84 ++++++++++++++++
 3 files changed, 242 insertions(+)
 create mode 100644 lib/private/db/migrationexception.php
 create mode 100644 lib/private/db/migrator.php
 create mode 100644 tests/lib/db/migrator.php

diff --git a/lib/private/db/migrationexception.php b/lib/private/db/migrationexception.php
new file mode 100644
index 00000000000..f257534812f
--- /dev/null
+++ b/lib/private/db/migrationexception.php
@@ -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);
+	}
+}
diff --git a/lib/private/db/migrator.php b/lib/private/db/migrator.php
new file mode 100644
index 00000000000..91060663f87
--- /dev/null
+++ b/lib/private/db/migrator.php
@@ -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));
+	}
+}
diff --git a/tests/lib/db/migrator.php b/tests/lib/db/migrator.php
new file mode 100644
index 00000000000..ed7c36dbdf4
--- /dev/null
+++ b/tests/lib/db/migrator.php
@@ -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);
+	}
+}
-- 
GitLab