aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/db
diff options
context:
space:
mode:
authoricewind1991 <robin@icewind.nl>2014-06-03 16:27:06 +0200
committericewind1991 <robin@icewind.nl>2014-06-03 16:27:06 +0200
commit68b15f3282fa0ef8c0f4e3a4e659a09eab02ef46 (patch)
tree811b9057d2a131b7a8d72e2bad2494ac230b0fef /lib/private/db
parenta5dd8e84f13c9815c0208851664238cfc0034b5b (diff)
parent4429b54ce4fdf5aa2936dd733f5c1751d342177e (diff)
downloadnextcloud-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.php95
-rw-r--r--lib/private/db/migrationexception.php26
-rw-r--r--lib/private/db/migrator.php196
-rw-r--r--lib/private/db/mysqlmigrator.php32
-rw-r--r--lib/private/db/nocheckmigrator.php24
-rw-r--r--lib/private/db/oraclemigrator.php40
-rw-r--r--lib/private/db/sqlitemigrator.php40
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());
+ }
+ }
+}