diff options
author | Morris Jobke <hey@morrisjobke.de> | 2017-07-05 17:32:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-05 17:32:40 +0200 |
commit | 60398b919be713ddb1813956061fee638c54317a (patch) | |
tree | 483d8f03b2a2b2b11cfebefbf0c3eb3c4b408d8f /lib | |
parent | ca565644b36930b63bb996268e848460843f0433 (diff) | |
parent | 163e8774a40bf35a650b41f1af415f89397511a5 (diff) | |
download | nextcloud-server-60398b919be713ddb1813956061fee638c54317a.tar.gz nextcloud-server-60398b919be713ddb1813956061fee638c54317a.zip |
Merge pull request #5231 from nextcloud/migrations
Migrations
Diffstat (limited to 'lib')
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 11 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 11 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/CodeChecker.php | 13 | ||||
-rw-r--r-- | lib/private/App/CodeChecker/MigrationSchemaChecker.php | 201 | ||||
-rw-r--r-- | lib/private/DB/Connection.php | 24 | ||||
-rw-r--r-- | lib/private/DB/MigrationService.php | 422 | ||||
-rw-r--r-- | lib/private/DB/Migrator.php | 17 | ||||
-rw-r--r-- | lib/private/DB/OracleConnection.php | 23 | ||||
-rw-r--r-- | lib/private/DB/OracleMigrator.php | 56 | ||||
-rw-r--r-- | lib/private/DB/SchemaWrapper.php | 139 | ||||
-rw-r--r-- | lib/private/Installer.php | 9 | ||||
-rw-r--r-- | lib/private/Migration/SimpleOutput.php | 84 | ||||
-rw-r--r-- | lib/private/Setup.php | 2 | ||||
-rw-r--r-- | lib/private/Setup/AbstractDatabase.php | 9 | ||||
-rw-r--r-- | lib/private/Updater.php | 6 | ||||
-rw-r--r-- | lib/private/legacy/app.php | 10 | ||||
-rw-r--r-- | lib/public/IDBConnection.php | 17 | ||||
-rw-r--r-- | lib/public/Migration/IMigrationStep.php | 55 | ||||
-rw-r--r-- | lib/public/Migration/SimpleMigrationStep.php | 59 |
19 files changed, 1148 insertions, 20 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 43ed4db9307..ee1dc08021c 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -220,8 +220,10 @@ return array( 'OCP\\Lockdown\\ILockdownManager' => $baseDir . '/lib/public/Lockdown/ILockdownManager.php', 'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php', + 'OCP\\Migration\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php', 'OCP\\Migration\\IRepairStep' => $baseDir . '/lib/public/Migration/IRepairStep.php', + 'OCP\\Migration\\SimpleMigrationStep' => $baseDir . '/lib/public/Migration/SimpleMigrationStep.php', 'OCP\\Notification\\IAction' => $baseDir . '/lib/public/Notification/IAction.php', 'OCP\\Notification\\IApp' => $baseDir . '/lib/public/Notification/IApp.php', 'OCP\\Notification\\IManager' => $baseDir . '/lib/public/Notification/IManager.php', @@ -339,6 +341,7 @@ return array( 'OC\\App\\CodeChecker\\ICheck' => $baseDir . '/lib/private/App/CodeChecker/ICheck.php', 'OC\\App\\CodeChecker\\InfoChecker' => $baseDir . '/lib/private/App/CodeChecker/InfoChecker.php', 'OC\\App\\CodeChecker\\LanguageParseChecker' => $baseDir . '/lib/private/App/CodeChecker/LanguageParseChecker.php', + 'OC\\App\\CodeChecker\\MigrationSchemaChecker' => $baseDir . '/lib/private/App/CodeChecker/MigrationSchemaChecker.php', 'OC\\App\\CodeChecker\\NodeVisitor' => $baseDir . '/lib/private/App/CodeChecker/NodeVisitor.php', 'OC\\App\\CodeChecker\\PrivateCheck' => $baseDir . '/lib/private/App/CodeChecker/PrivateCheck.php', 'OC\\App\\CodeChecker\\StrongComparisonCheck' => $baseDir . '/lib/private/App/CodeChecker/StrongComparisonCheck.php', @@ -420,6 +423,10 @@ return array( 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => $baseDir . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => $baseDir . '/core/Command/Db/ConvertType.php', 'OC\\Core\\Command\\Db\\GenerateChangeScript' => $baseDir . '/core/Command/Db/GenerateChangeScript.php', + 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => $baseDir . '/core/Command/Db/Migrations/ExecuteCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => $baseDir . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => $baseDir . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => $baseDir . '/core/Command/Encryption/DecryptAll.php', 'OC\\Core\\Command\\Encryption\\Disable' => $baseDir . '/core/Command/Encryption/Disable.php', @@ -480,6 +487,7 @@ return array( 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => $baseDir . '/core/Middleware/TwoFactorMiddleware.php', + 'OC\\Core\\Migrations\\Version13000Date20170705121758' => $baseDir . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', @@ -491,6 +499,7 @@ return array( 'OC\\DB\\MDB2SchemaReader' => $baseDir . '/lib/private/DB/MDB2SchemaReader.php', 'OC\\DB\\MDB2SchemaWriter' => $baseDir . '/lib/private/DB/MDB2SchemaWriter.php', 'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php', + 'OC\\DB\\MigrationService' => $baseDir . '/lib/private/DB/MigrationService.php', 'OC\\DB\\Migrator' => $baseDir . '/lib/private/DB/Migrator.php', 'OC\\DB\\MySQLMigrator' => $baseDir . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => $baseDir . '/lib/private/DB/MySqlTools.php', @@ -517,6 +526,7 @@ return array( 'OC\\DB\\QueryBuilder\\QuoteHelper' => $baseDir . '/lib/private/DB/QueryBuilder/QuoteHelper.php', 'OC\\DB\\SQLiteMigrator' => $baseDir . '/lib/private/DB/SQLiteMigrator.php', 'OC\\DB\\SQLiteSessionInit' => $baseDir . '/lib/private/DB/SQLiteSessionInit.php', + 'OC\\DB\\SchemaWrapper' => $baseDir . '/lib/private/DB/SchemaWrapper.php', 'OC\\DatabaseException' => $baseDir . '/lib/private/DatabaseException.php', 'OC\\DatabaseSetupException' => $baseDir . '/lib/private/DatabaseSetupException.php', 'OC\\DateTimeFormatter' => $baseDir . '/lib/private/DateTimeFormatter.php', @@ -682,6 +692,7 @@ return array( 'OC\\Memcache\\XCache' => $baseDir . '/lib/private/Memcache/XCache.php', 'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => $baseDir . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\SimpleOutput' => $baseDir . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => $baseDir . '/lib/private/NaturalSort.php', 'OC\\NaturalSort_DefaultCollator' => $baseDir . '/lib/private/NaturalSort_DefaultCollator.php', 'OC\\NavigationManager' => $baseDir . '/lib/private/NavigationManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 00b7908f848..7b133d54a20 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -250,8 +250,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Lockdown\\ILockdownManager' => __DIR__ . '/../../..' . '/lib/public/Lockdown/ILockdownManager.php', 'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php', 'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php', + 'OCP\\Migration\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php', 'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php', 'OCP\\Migration\\IRepairStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IRepairStep.php', + 'OCP\\Migration\\SimpleMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/SimpleMigrationStep.php', 'OCP\\Notification\\IAction' => __DIR__ . '/../../..' . '/lib/public/Notification/IAction.php', 'OCP\\Notification\\IApp' => __DIR__ . '/../../..' . '/lib/public/Notification/IApp.php', 'OCP\\Notification\\IManager' => __DIR__ . '/../../..' . '/lib/public/Notification/IManager.php', @@ -369,6 +371,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\App\\CodeChecker\\ICheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/ICheck.php', 'OC\\App\\CodeChecker\\InfoChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/InfoChecker.php', 'OC\\App\\CodeChecker\\LanguageParseChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/LanguageParseChecker.php', + 'OC\\App\\CodeChecker\\MigrationSchemaChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/MigrationSchemaChecker.php', 'OC\\App\\CodeChecker\\NodeVisitor' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/NodeVisitor.php', 'OC\\App\\CodeChecker\\PrivateCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/PrivateCheck.php', 'OC\\App\\CodeChecker\\StrongComparisonCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/StrongComparisonCheck.php', @@ -450,6 +453,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\Db\\ConvertMysqlToMB4' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertMysqlToMB4.php', 'OC\\Core\\Command\\Db\\ConvertType' => __DIR__ . '/../../..' . '/core/Command/Db/ConvertType.php', 'OC\\Core\\Command\\Db\\GenerateChangeScript' => __DIR__ . '/../../..' . '/core/Command/Db/GenerateChangeScript.php', + 'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/ExecuteCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/MigrateCommand.php', + 'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/StatusCommand.php', 'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ChangeKeyStorageRoot.php', 'OC\\Core\\Command\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/core/Command/Encryption/DecryptAll.php', 'OC\\Core\\Command\\Encryption\\Disable' => __DIR__ . '/../../..' . '/core/Command/Encryption/Disable.php', @@ -510,6 +517,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => __DIR__ . '/../../..' . '/core/Middleware/TwoFactorMiddleware.php', + 'OC\\Core\\Migrations\\Version13000Date20170705121758' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', @@ -521,6 +529,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\MDB2SchemaReader' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaReader.php', 'OC\\DB\\MDB2SchemaWriter' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaWriter.php', 'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php', + 'OC\\DB\\MigrationService' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationService.php', 'OC\\DB\\Migrator' => __DIR__ . '/../../..' . '/lib/private/DB/Migrator.php', 'OC\\DB\\MySQLMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/MySQLMigrator.php', 'OC\\DB\\MySqlTools' => __DIR__ . '/../../..' . '/lib/private/DB/MySqlTools.php', @@ -547,6 +556,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\DB\\QueryBuilder\\QuoteHelper' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/QuoteHelper.php', 'OC\\DB\\SQLiteMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteMigrator.php', 'OC\\DB\\SQLiteSessionInit' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteSessionInit.php', + 'OC\\DB\\SchemaWrapper' => __DIR__ . '/../../..' . '/lib/private/DB/SchemaWrapper.php', 'OC\\DatabaseException' => __DIR__ . '/../../..' . '/lib/private/DatabaseException.php', 'OC\\DatabaseSetupException' => __DIR__ . '/../../..' . '/lib/private/DatabaseSetupException.php', 'OC\\DateTimeFormatter' => __DIR__ . '/../../..' . '/lib/private/DateTimeFormatter.php', @@ -712,6 +722,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Memcache\\XCache' => __DIR__ . '/../../..' . '/lib/private/Memcache/XCache.php', 'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php', 'OC\\Migration\\ConsoleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/ConsoleOutput.php', + 'OC\\Migration\\SimpleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/SimpleOutput.php', 'OC\\NaturalSort' => __DIR__ . '/../../..' . '/lib/private/NaturalSort.php', 'OC\\NaturalSort_DefaultCollator' => __DIR__ . '/../../..' . '/lib/private/NaturalSort_DefaultCollator.php', 'OC\\NavigationManager' => __DIR__ . '/../../..' . '/lib/private/NavigationManager.php', diff --git a/lib/private/App/CodeChecker/CodeChecker.php b/lib/private/App/CodeChecker/CodeChecker.php index 291bedee92f..a6368ab683f 100644 --- a/lib/private/App/CodeChecker/CodeChecker.php +++ b/lib/private/App/CodeChecker/CodeChecker.php @@ -51,8 +51,12 @@ class CodeChecker extends BasicEmitter { /** @var ICheck */ protected $checkList; - public function __construct(ICheck $checkList) { + /** @var bool */ + protected $checkMigrationSchema; + + public function __construct(ICheck $checkList, $checkMigrationSchema) { $this->checkList = $checkList; + $this->checkMigrationSchema = $checkMigrationSchema; $this->parser = new Parser(new Lexer); } @@ -120,11 +124,16 @@ class CodeChecker extends BasicEmitter { $statements = $this->parser->parse($code); $visitor = new NodeVisitor($this->checkList); + $migrationVisitor = new MigrationSchemaChecker(); $traverser = new NodeTraverser; $traverser->addVisitor($visitor); + if ($this->checkMigrationSchema && preg_match('#^.+\\/Migration\\/Version[^\\/]{1,255}\\.php$#i', $file)) { + $traverser->addVisitor($migrationVisitor); + } + $traverser->traverse($statements); - return $visitor->errors; + return array_merge($visitor->errors, $migrationVisitor->errors); } } diff --git a/lib/private/App/CodeChecker/MigrationSchemaChecker.php b/lib/private/App/CodeChecker/MigrationSchemaChecker.php new file mode 100644 index 00000000000..9dee358327d --- /dev/null +++ b/lib/private/App/CodeChecker/MigrationSchemaChecker.php @@ -0,0 +1,201 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\CodeChecker; + +use PhpParser\Node; +use PhpParser\Node\Name; +use PhpParser\NodeVisitorAbstract; + +class MigrationSchemaChecker extends NodeVisitorAbstract { + + /** @var string */ + protected $schemaVariableName = null; + /** @var array */ + protected $tableVariableNames = []; + /** @var array */ + public $errors = []; + + public function enterNode(Node $node) { + /** + * Check tables + */ + if ($this->schemaVariableName !== null && + $node instanceof Node\Expr\Assign && + $node->var instanceof Node\Expr\Variable && + $node->expr instanceof Node\Expr\MethodCall && + $node->expr->var instanceof Node\Expr\Variable && + $node->expr->var->name === $this->schemaVariableName) { + + if ($node->expr->name === 'createTable') { + if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->expr->args[0]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->expr->args[0]->value->value, + 'reason' => 'Table name is too long (max. 27)', + ]; + } else { + $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value; + } + } + } else if ($node->expr->name === 'getTable') { + if (isset($node->expr->args[0]) && $node->expr->args[0]->value instanceof Node\Scalar\String_) { + $this->tableVariableNames[$node->var->name] = $node->expr->args[0]->value->value; + } + } + } else if ($this->schemaVariableName !== null && + $node instanceof Node\Expr\MethodCall && + $node->var instanceof Node\Expr\Variable && + $node->var->name === $this->schemaVariableName) { + + if ($node->name === 'renameTable') { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => 'Deprecated method', + 'reason' => sprintf( + '`$%s->renameTable()` must not be used', + $node->var->name + ), + ]; + } + + /** + * Check columns and Indexes + */ + } else if (!empty($this->tableVariableNames) && + $node instanceof Node\Expr\MethodCall && + $node->var instanceof Node\Expr\Variable && + isset($this->tableVariableNames[$node->var->name])) { + + if ($node->name === 'addColumn' || $node->name === 'changeColumn') { + if (isset($node->args[0]) && $node->args[0]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[0]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[0]->value->value, + 'reason' => sprintf( + 'Column name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + + // On autoincrement the max length of the table name is 21 instead of 27 + if (isset($node->args[2]) && $node->args[2]->value instanceof Node\Expr\Array_) { + /** @var Node\Expr\Array_ $options */ + $options = $node->args[2]->value; + if ($this->checkColumnForAutoincrement($options)) { + if (!$this->checkNameLength($this->tableVariableNames[$node->var->name], true)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $this->tableVariableNames[$node->var->name], + 'reason' => 'Table name is too long because of autoincrement (max. 21)', + ]; + } + } + } + } + } else if ($node->name === 'addIndex' || + $node->name === 'addUniqueIndex' || + $node->name === 'renameIndex' || + $node->name === 'setPrimaryKey') { + if (isset($node->args[1]) && $node->args[1]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[1]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[1]->value->value, + 'reason' => sprintf( + 'Index name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + } + } else if ($node->name === 'addForeignKeyConstraint') { + if (isset($node->args[4]) && $node->args[4]->value instanceof Node\Scalar\String_) { + if (!$this->checkNameLength($node->args[4]->value->value)) { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => $node->args[4]->value->value, + 'reason' => sprintf( + 'Constraint name is too long on table `%s` (max. 27)', + $this->tableVariableNames[$node->var->name] + ), + ]; + } + } + } else if ($node->name === 'renameColumn') { + $this->errors[] = [ + 'line' => $node->getLine(), + 'disallowedToken' => 'Deprecated method', + 'reason' => sprintf( + '`$%s->renameColumn()` must not be used', + $node->var->name + ), + ]; + } + + /** + * Find the schema + */ + } else if ($node instanceof Node\Expr\Assign && + $node->expr instanceof Node\Expr\FuncCall && + $node->var instanceof Node\Expr\Variable && + $node->expr->name instanceof Node\Expr\Variable && + $node->expr->name->name === 'schemaClosure') { + // E.g. $schema = $schemaClosure(); + $this->schemaVariableName = $node->var->name; + } + } + + protected function checkNameLength($tableName, $hasAutoincrement = false) { + if ($hasAutoincrement) { + return strlen($tableName) <= 21; + } + return strlen($tableName) <= 27; + } + + /** + * @param Node\Expr\Array_ $optionsArray + * @return bool Whether the column is an autoincrement column + */ + protected function checkColumnForAutoincrement(Node\Expr\Array_ $optionsArray) { + foreach ($optionsArray->items as $option) { + if ($option->key instanceof Node\Scalar\String_) { + if ($option->key->value === 'autoincrement' && + $option->value instanceof Node\Expr\ConstFetch) { + /** @var Node\Expr\ConstFetch $const */ + $const = $option->value; + + if ($const->name instanceof Name && + $const->name->parts === ['true']) { + return true; + } + } + } + } + + return false; + } +} diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 6b56ae8ad5c..563c077b04a 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -35,6 +35,7 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\Common\EventManager; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Exception\ConstraintViolationException; +use Doctrine\DBAL\Schema\Schema; use OC\DB\QueryBuilder\QueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -418,4 +419,27 @@ class Connection extends \Doctrine\DBAL\Connection implements IDBConnection { } return $this->getParams()['charset'] === 'utf8mb4'; } + + + /** + * Create the schema of the connected database + * + * @return Schema + */ + public function createSchema() { + $schemaManager = new MDB2SchemaManager($this); + $migrator = $schemaManager->getMigrator(); + return $migrator->createSchema(); + } + + /** + * Migrate the database to the given schema + * + * @param Schema $toSchema + */ + public function migrateToSchema(Schema $toSchema) { + $schemaManager = new MDB2SchemaManager($this); + $migrator = $schemaManager->getMigrator(); + $migrator->migrate($toSchema); + } } diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php new file mode 100644 index 00000000000..a4276b08c12 --- /dev/null +++ b/lib/private/DB/MigrationService.php @@ -0,0 +1,422 @@ +<?php +/** + * @author Joas Schilling <coding@schilljs.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * @copyright Copyright (c) 2017, ownCloud GmbH + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; +use OC\IntegrityCheck\Helpers\AppLocator; +use OC\Migration\SimpleOutput; +use OCP\AppFramework\App; +use OCP\AppFramework\QueryException; +use OCP\IDBConnection; +use OCP\Migration\IMigrationStep; +use OCP\Migration\IOutput; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\Type; + +class MigrationService { + + /** @var boolean */ + private $migrationTableCreated; + /** @var array */ + private $migrations; + /** @var IOutput */ + private $output; + /** @var Connection */ + private $connection; + /** @var string */ + private $appName; + + /** + * MigrationService constructor. + * + * @param $appName + * @param IDBConnection $connection + * @param AppLocator $appLocator + * @param IOutput|null $output + * @throws \Exception + */ + public function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) { + $this->appName = $appName; + $this->connection = $connection; + $this->output = $output; + if (null === $this->output) { + $this->output = new SimpleOutput(\OC::$server->getLogger(), $appName); + } + + if ($appName === 'core') { + $this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations'; + $this->migrationsNamespace = 'OC\\Core\\Migrations'; + } else { + if (null === $appLocator) { + $appLocator = new AppLocator(); + } + $appPath = $appLocator->getAppPath($appName); + $namespace = App::buildAppNamespace($appName); + $this->migrationsPath = "$appPath/lib/Migration"; + $this->migrationsNamespace = $namespace . '\\Migration'; + + if (!@mkdir($appPath . '/lib') && !is_dir($appPath . '/lib')) { + throw new \RuntimeException("Could not create migration folder \"{$this->migrationsPath}\""); + } + } + + if (!@mkdir($this->migrationsPath) && !is_dir($this->migrationsPath)) { + throw new \RuntimeException("Could not create migration folder \"{$this->migrationsPath}\""); + } + } + + /** + * Returns the name of the app for which this migration is executed + * + * @return string + */ + public function getApp() { + return $this->appName; + } + + /** + * @return bool + * @codeCoverageIgnore - this will implicitly tested on installation + */ + private function createMigrationTable() { + if ($this->migrationTableCreated) { + return false; + } + + if ($this->connection->tableExists('migrations')) { + $this->migrationTableCreated = true; + return false; + } + + $tableName = $this->connection->getPrefix() . 'migrations'; + $tableName = $this->connection->getDatabasePlatform()->quoteIdentifier($tableName); + + $columns = [ + 'app' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('app'), Type::getType('string'), ['length' => 255]), + 'version' => new Column($this->connection->getDatabasePlatform()->quoteIdentifier('version'), Type::getType('string'), ['length' => 255]), + ]; + $table = new Table($tableName, $columns); + $table->setPrimaryKey([ + $this->connection->getDatabasePlatform()->quoteIdentifier('app'), + $this->connection->getDatabasePlatform()->quoteIdentifier('version')]); + $this->connection->getSchemaManager()->createTable($table); + + $this->migrationTableCreated = true; + + return true; + } + + /** + * Returns all versions which have already been applied + * + * @return string[] + * @codeCoverageIgnore - no need to test this + */ + public function getMigratedVersions() { + $this->createMigrationTable(); + $qb = $this->connection->getQueryBuilder(); + + $qb->select('version') + ->from('migrations') + ->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp()))) + ->orderBy('version'); + + $result = $qb->execute(); + $rows = $result->fetchAll(\PDO::FETCH_COLUMN); + $result->closeCursor(); + + return $rows; + } + + /** + * Returns all versions which are available in the migration folder + * + * @return array + */ + public function getAvailableVersions() { + $this->ensureMigrationsAreLoaded(); + return array_map('strval', array_keys($this->migrations)); + } + + protected function findMigrations() { + $directory = realpath($this->migrationsPath); + $iterator = new \RegexIterator( + new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ), + '#^.+\\/Version[^\\/]{1,255}\\.php$#i', + \RegexIterator::GET_MATCH); + + $files = array_keys(iterator_to_array($iterator)); + uasort($files, function ($a, $b) { + preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA); + preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB); + if (!empty($matchA) && !empty($matchB)) { + if ($matchA[1] !== $matchB[1]) { + return ($matchA[1] < $matchB[1]) ? -1 : 1; + } + return ($matchA[2] < $matchB[2]) ? -1 : 1; + } + return (basename($a) < basename($b)) ? -1 : 1; + }); + + $migrations = []; + + foreach ($files as $file) { + $className = basename($file, '.php'); + $version = (string) substr($className, 7); + if ($version === '0') { + throw new \InvalidArgumentException( + "Cannot load a migrations with the name '$version' because it is a reserved number" + ); + } + $migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className); + } + + return $migrations; + } + + /** + * @param string $to + * @return string[] + */ + private function getMigrationsToExecute($to) { + $knownMigrations = $this->getMigratedVersions(); + $availableMigrations = $this->getAvailableVersions(); + + $toBeExecuted = []; + foreach ($availableMigrations as $v) { + if ($to !== 'latest' && $v > $to) { + continue; + } + if ($this->shallBeExecuted($v, $knownMigrations)) { + $toBeExecuted[] = $v; + } + } + + return $toBeExecuted; + } + + /** + * @param string $m + * @param string[] $knownMigrations + * @return bool + */ + private function shallBeExecuted($m, $knownMigrations) { + if (in_array($m, $knownMigrations)) { + return false; + } + + return true; + } + + /** + * @param string $version + */ + private function markAsExecuted($version) { + $this->connection->insertIfNotExist('*PREFIX*migrations', [ + 'app' => $this->appName, + 'version' => $version + ]); + } + + /** + * Returns the name of the table which holds the already applied versions + * + * @return string + */ + public function getMigrationsTableName() { + return $this->connection->getPrefix() . 'migrations'; + } + + /** + * Returns the namespace of the version classes + * + * @return string + */ + public function getMigrationsNamespace() { + return $this->migrationsNamespace; + } + + /** + * Returns the directory which holds the versions + * + * @return string + */ + public function getMigrationsDirectory() { + return $this->migrationsPath; + } + + /** + * Return the explicit version for the aliases; current, next, prev, latest + * + * @param string $alias + * @return mixed|null|string + */ + public function getMigration($alias) { + switch($alias) { + case 'current': + return $this->getCurrentVersion(); + case 'next': + return $this->getRelativeVersion($this->getCurrentVersion(), 1); + case 'prev': + return $this->getRelativeVersion($this->getCurrentVersion(), -1); + case 'latest': + $this->ensureMigrationsAreLoaded(); + + return @end($this->getAvailableVersions()); + } + return '0'; + } + + /** + * @param string $version + * @param int $delta + * @return null|string + */ + private function getRelativeVersion($version, $delta) { + $this->ensureMigrationsAreLoaded(); + + $versions = $this->getAvailableVersions(); + array_unshift($versions, 0); + $offset = array_search($version, $versions, true); + if ($offset === false || !isset($versions[$offset + $delta])) { + // Unknown version or delta out of bounds. + return null; + } + + return (string) $versions[$offset + $delta]; + } + + /** + * @return string + */ + private function getCurrentVersion() { + $m = $this->getMigratedVersions(); + if (count($m) === 0) { + return '0'; + } + return @end(array_values($m)); + } + + /** + * @param string $version + * @return string + * @throws \InvalidArgumentException + */ + private function getClass($version) { + $this->ensureMigrationsAreLoaded(); + + if (isset($this->migrations[$version])) { + return $this->migrations[$version]; + } + + throw new \InvalidArgumentException("Version $version is unknown."); + } + + /** + * Allows to set an IOutput implementation which is used for logging progress and messages + * + * @param IOutput $output + */ + public function setOutput(IOutput $output) { + $this->output = $output; + } + + /** + * Applies all not yet applied versions up to $to + * + * @param string $to + * @throws \InvalidArgumentException + */ + public function migrate($to = 'latest') { + // read known migrations + $toBeExecuted = $this->getMigrationsToExecute($to); + foreach ($toBeExecuted as $version) { + $this->executeStep($version); + } + } + + /** + * @param string $version + * @return mixed + * @throws \InvalidArgumentException + */ + protected function createInstance($version) { + $class = $this->getClass($version); + try { + $s = \OC::$server->query($class); + } catch (QueryException $e) { + if (class_exists($class)) { + $s = new $class(); + } else { + throw new \InvalidArgumentException("Migration step '$class' is unknown"); + } + } + + return $s; + } + + /** + * Executes one explicit version + * + * @param string $version + * @throws \InvalidArgumentException + */ + public function executeStep($version) { + $instance = $this->createInstance($version); + if (!$instance instanceof IMigrationStep) { + throw new \InvalidArgumentException('Not a valid migration'); + } + + $instance->preSchemaChange($this->output, function() { + return $this->connection->createSchema(); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + $toSchema = $instance->changeSchema($this->output, function() { + return new SchemaWrapper($this->connection); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + if ($toSchema instanceof SchemaWrapper) { + $this->connection->migrateToSchema($toSchema->getWrappedSchema()); + $toSchema->performDropTableCalls(); + } + + $instance->postSchemaChange($this->output, function() { + return $this->connection->createSchema(); + }, ['tablePrefix' => $this->connection->getPrefix()]); + + $this->markAsExecuted($version); + } + + private function ensureMigrationsAreLoaded() { + if (empty($this->migrations)) { + $this->migrations = $this->findMigrations(); + } + } +} diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php index 1d00d9a1b45..da381ba0284 100644 --- a/lib/private/DB/Migrator.php +++ b/lib/private/DB/Migrator.php @@ -43,14 +43,10 @@ use Symfony\Component\EventDispatcher\GenericEvent; class Migrator { - /** - * @var \Doctrine\DBAL\Connection $connection - */ + /** @var \Doctrine\DBAL\Connection */ protected $connection; - /** - * @var ISecureRandom - */ + /** @var ISecureRandom */ private $random; /** @var IConfig */ @@ -197,6 +193,12 @@ class Migrator { return new Table($newName, $table->getColumns(), $newIndexes, array(), 0, $table->getOptions()); } + public function createSchema() { + $filterExpression = $this->getFilterExpression(); + $this->connection->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); + return $this->connection->getSchemaManager()->createSchema(); + } + /** * @param Schema $targetSchema * @param \Doctrine\DBAL\Connection $connection @@ -217,8 +219,7 @@ class Migrator { } $filterExpression = $this->getFilterExpression(); - $this->connection->getConfiguration()-> - setFilterSchemaAssetsExpression($filterExpression); + $this->connection->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); $sourceSchema = $connection->getSchemaManager()->createSchema(); // remove tables we don't know about diff --git a/lib/private/DB/OracleConnection.php b/lib/private/DB/OracleConnection.php index 08d71365172..51faf21970c 100644 --- a/lib/private/DB/OracleConnection.php +++ b/lib/private/DB/OracleConnection.php @@ -30,9 +30,14 @@ class OracleConnection extends Connection { * Quote the keys of the array */ private function quoteKeys(array $data) { - $return = array(); + $return = []; + $c = $this->getDatabasePlatform()->getIdentifierQuoteCharacter(); foreach($data as $key => $value) { - $return[$this->quoteIdentifier($key)] = $value; + if ($key[0] !== $c) { + $return[$this->quoteIdentifier($key)] = $value; + } else { + $return[$key] = $value; + } } return $return; } @@ -41,7 +46,9 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function insert($tableName, array $data, array $types = array()) { - $tableName = $this->quoteIdentifier($tableName); + if ($tableName[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableName = $this->quoteIdentifier($tableName); + } $data = $this->quoteKeys($data); return parent::insert($tableName, $data, $types); } @@ -50,7 +57,9 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function update($tableName, array $data, array $identifier, array $types = array()) { - $tableName = $this->quoteIdentifier($tableName); + if ($tableName[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableName = $this->quoteIdentifier($tableName); + } $data = $this->quoteKeys($data); $identifier = $this->quoteKeys($identifier); return parent::update($tableName, $data, $identifier, $types); @@ -60,9 +69,11 @@ class OracleConnection extends Connection { * {@inheritDoc} */ public function delete($tableExpression, array $identifier, array $types = array()) { - $tableName = $this->quoteIdentifier($tableExpression); + if ($tableExpression[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) { + $tableExpression = $this->quoteIdentifier($tableExpression); + } $identifier = $this->quoteKeys($identifier); - return parent::delete($tableName, $identifier); + return parent::delete($tableExpression, $identifier); } /** diff --git a/lib/private/DB/OracleMigrator.php b/lib/private/DB/OracleMigrator.php index 908b2dedf03..2735529b5e2 100644 --- a/lib/private/DB/OracleMigrator.php +++ b/lib/private/DB/OracleMigrator.php @@ -24,19 +24,75 @@ namespace OC\DB; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ColumnDiff; +use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; class OracleMigrator extends NoCheckMigrator { /** * @param Schema $targetSchema * @param \Doctrine\DBAL\Connection $connection * @return \Doctrine\DBAL\Schema\SchemaDiff + * @throws DBALException */ protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { $schemaDiff = parent::getDiff($targetSchema, $connection); // oracle forces us to quote the identifiers + $schemaDiff->newTables = array_map(function(Table $table) { + return new Table( + $this->connection->quoteIdentifier($table->getName()), + array_map(function(Column $column) { + $newColumn = new Column( + $this->connection->quoteIdentifier($column->getName()), + $column->getType() + ); + $newColumn->setAutoincrement($column->getAutoincrement()); + $newColumn->setColumnDefinition($column->getColumnDefinition()); + $newColumn->setComment($column->getComment()); + $newColumn->setDefault($column->getDefault()); + $newColumn->setFixed($column->getFixed()); + $newColumn->setLength($column->getLength()); + $newColumn->setNotnull($column->getNotnull()); + $newColumn->setPrecision($column->getPrecision()); + $newColumn->setScale($column->getScale()); + $newColumn->setUnsigned($column->getUnsigned()); + $newColumn->setPlatformOptions($column->getPlatformOptions()); + $newColumn->setCustomSchemaOptions($column->getPlatformOptions()); + return $newColumn; + }, $table->getColumns()), + array_map(function(Index $index) { + return new Index( + $this->connection->quoteIdentifier($index->getName()), + array_map(function($columnName) { + return $this->connection->quoteIdentifier($columnName); + }, $index->getColumns()), + $index->isUnique(), + $index->isPrimary(), + $index->getFlags(), + $index->getOptions() + ); + }, $table->getIndexes()), + $table->getForeignKeys(), + 0, + $table->getOptions() + ); + }, $schemaDiff->newTables); + + $schemaDiff->removedTables = array_map(function(Table $table) { + return new Table( + $this->connection->quoteIdentifier($table->getName()), + $table->getColumns(), + $table->getIndexes(), + $table->getForeignKeys(), + 0, + $table->getOptions() + ); + }, $schemaDiff->removedTables); + foreach ($schemaDiff->changedTables as $tableDiff) { $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); foreach ($tableDiff->changedColumns as $column) { diff --git a/lib/private/DB/SchemaWrapper.php b/lib/private/DB/SchemaWrapper.php new file mode 100644 index 00000000000..0be45d8d3f7 --- /dev/null +++ b/lib/private/DB/SchemaWrapper.php @@ -0,0 +1,139 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Schema; +use OCP\IDBConnection; + +class SchemaWrapper { + + /** @var IDBConnection|Connection */ + protected $connection; + + /** @var Schema */ + protected $schema; + + /** @var array */ + protected $tablesToDelete; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + $this->schema = $this->connection->createSchema(); + } + + public function getWrappedSchema() { + return $this->schema; + } + + public function performDropTableCalls() { + foreach ($this->tablesToDelete as $tableName => $true) { + $this->connection->dropTable($tableName); + unset($this->tablesToDelete[$tableName]); + } + } + + /** + * Gets all table names + * + * @return array + */ + public function getTableNamesWithoutPrefix() { + $tableNames = $this->schema->getTableNames(); + return array_map(function($tableName) { + if (strpos($tableName, $this->connection->getPrefix()) === 0) { + return substr($tableName, strlen($this->connection->getPrefix())); + } + + return $tableName; + }, $tableNames); + } + + // Overwritten methods + + /** + * @param string $tableName + * + * @return \Doctrine\DBAL\Schema\Table + * @throws \Doctrine\DBAL\Schema\SchemaException + */ + public function getTable($tableName) { + return $this->schema->getTable($this->connection->getPrefix() . $tableName); + } + + /** + * Does this schema have a table with the given name? + * + * @param string $tableName + * + * @return boolean + */ + public function hasTable($tableName) { + return $this->schema->hasTable($this->connection->getPrefix() . $tableName); + } + + /** + * Creates a new table. + * + * @param string $tableName + * @return \Doctrine\DBAL\Schema\Table + */ + public function createTable($tableName) { + return $this->schema->createTable($this->connection->getPrefix() . $tableName); + } + + /** + * Renames a table. + * + * @param string $oldTableName + * @param string $newTableName + * + * @return \Doctrine\DBAL\Schema\Schema + * @throws DBALException + */ + public function renameTable($oldTableName, $newTableName) { + throw new DBALException('Renaming tables is not supported. Please create and drop the tables manually.'); + } + + /** + * Drops a table from the schema. + * + * @param string $tableName + * @return \Doctrine\DBAL\Schema\Schema + */ + public function dropTable($tableName) { + $this->tablesToDelete[$tableName] = true; + return $this->schema->dropTable($this->connection->getPrefix() . $tableName); + } + + /** + * @param string $name + * @param array $arguments + * @return mixed + */ + public function __call($name, $arguments) { + return call_user_func_array([$this->schema, $name], $arguments); + } +} diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 35f51b19b07..60b0336fb5a 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -139,6 +139,9 @@ class Installer { } else { OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml'); } + } else { + $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection()); + $ms->migrate(); } \OC_App::registerAutoloading($appId, $basedir); @@ -530,6 +533,8 @@ class Installer { public static function installShippedApp($app) { //install the database $appPath = OC_App::getAppPath($app); + \OC_App::registerAutoloading($app, $appPath); + if(is_file("$appPath/appinfo/database.xml")) { try { OC_DB::createDbFromStructure("$appPath/appinfo/database.xml"); @@ -540,10 +545,12 @@ class Installer { 0, $e ); } + } else { + $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection()); + $ms->migrate(); } //run appinfo/install.php - \OC_App::registerAutoloading($app, $appPath); self::includeAppScript("$appPath/appinfo/install.php"); $info = OC_App::getAppInfo($app); diff --git a/lib/private/Migration/SimpleOutput.php b/lib/private/Migration/SimpleOutput.php new file mode 100644 index 00000000000..b28fcbd7628 --- /dev/null +++ b/lib/private/Migration/SimpleOutput.php @@ -0,0 +1,84 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\Migration; + + +use OCP\ILogger; +use OCP\Migration\IOutput; + +/** + * Class SimpleOutput + * + * Just a simple IOutput implementation with writes messages to the log file. + * Alternative implementations will write to the console or to the web ui (web update case) + * + * @package OC\Migration + */ +class SimpleOutput implements IOutput { + + /** @var ILogger */ + private $logger; + private $appName; + + public function __construct(ILogger $logger, $appName) { + $this->logger = $logger; + $this->appName = $appName; + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function info($message) { + $this->logger->info($message, ['app' => $this->appName]); + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function warning($message) { + $this->logger->warning($message, ['app' => $this->appName]); + } + + /** + * @param int $max + * @since 9.1.0 + */ + public function startProgress($max = 0) { + } + + /** + * @param int $step + * @param string $description + * @since 9.1.0 + */ + public function advance($step = 1, $description = '') { + } + + /** + * @since 9.1.0 + */ + public function finishProgress() { + } +} diff --git a/lib/private/Setup.php b/lib/private/Setup.php index b8a861fd296..5cd3c84ce92 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -332,6 +332,8 @@ class Setup { try { $dbSetup->initialize($options); $dbSetup->setupDatabase($username); + // apply necessary migrations + $dbSetup->runMigrations(); } catch (\OC\DatabaseSetupException $e) { $error[] = array( 'error' => $e->getMessage(), diff --git a/lib/private/Setup/AbstractDatabase.php b/lib/private/Setup/AbstractDatabase.php index d5c34291e60..2fbec326a5d 100644 --- a/lib/private/Setup/AbstractDatabase.php +++ b/lib/private/Setup/AbstractDatabase.php @@ -27,6 +27,7 @@ namespace OC\Setup; use OC\DB\ConnectionFactory; +use OC\DB\MigrationService; use OC\SystemConfig; use OCP\IL10N; use OCP\ILogger; @@ -143,4 +144,12 @@ abstract class AbstractDatabase { * @param string $userName */ abstract public function setupDatabase($userName); + + public function runMigrations() { + if (!is_dir(\OC::$SERVERROOT."/core/Migrations")) { + return; + } + $ms = new MigrationService('core', \OC::$server->getDatabaseConnection()); + $ms->migrate(); + } } diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 6d08e5d4cc0..6c23a43c354 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -32,6 +32,7 @@ namespace OC; +use OC\DB\MigrationService; use OC\Hooks\BasicEmitter; use OC\IntegrityCheck\Checker; use OC_App; @@ -300,8 +301,9 @@ class Updater extends BasicEmitter { protected function doCoreUpgrade() { $this->emit('\OC\Updater', 'dbUpgradeBefore'); - // do the real upgrade - \OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml'); + // execute core migrations + $ms = new MigrationService('core', \OC::$server->getDatabaseConnection()); + $ms->migrate(); $this->emit('\OC\Updater', 'dbUpgrade'); } diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index 1bdbd1e2a83..463e13da915 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -50,6 +50,7 @@ use OC\App\DependencyAnalyzer; use OC\App\InfoParser; use OC\App\Platform; +use OC\DB\MigrationService; use OC\Installer; use OC\Repair; use OCP\App\ManagerEvent; @@ -1041,20 +1042,27 @@ class OC_App { if($appPath === false) { return false; } + self::registerAutoloading($appId, $appPath); + $appData = self::getAppInfo($appId); self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); + if (file_exists($appPath . '/appinfo/database.xml')) { OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml'); + } else { + $ms = new MigrationService($appId, \OC::$server->getDatabaseConnection()); + $ms->migrate(); } + self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); unset(self::$appVersion[$appId]); + // run upgrade code if (file_exists($appPath . '/appinfo/update.php')) { self::loadApp($appId); include $appPath . '/appinfo/update.php'; } - self::registerAutoloading($appId, $appPath); self::setupBackgroundJobs($appData['background-jobs']); if(isset($appData['settings']) && is_array($appData['settings'])) { \OC::$server->getSettingsManager()->setupSettings($appData['settings']); diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php index efd65d55f7e..56cf50c5fb3 100644 --- a/lib/public/IDBConnection.php +++ b/lib/public/IDBConnection.php @@ -34,6 +34,7 @@ // use OCP namespace for all classes that are considered public. // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP; +use Doctrine\DBAL\Schema\Schema; use OCP\DB\QueryBuilder\IQueryBuilder; /** @@ -259,4 +260,20 @@ interface IDBConnection { * @since 11.0.0 */ public function supports4ByteText(); + + /** + * Create the schema of the connected database + * + * @return Schema + * @since 13.0.0 + */ + public function createSchema(); + + /** + * Migrate the database to the given schema + * + * @param Schema $toSchema + * @since 13.0.0 + */ + public function migrateToSchema(Schema $toSchema); } diff --git a/lib/public/Migration/IMigrationStep.php b/lib/public/Migration/IMigrationStep.php new file mode 100644 index 00000000000..49bb236ab7b --- /dev/null +++ b/lib/public/Migration/IMigrationStep.php @@ -0,0 +1,55 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Migration; + +use Doctrine\DBAL\Schema\Schema; + +/** + * @since 13.0.0 + */ +interface IMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options); + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options); + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options); +} diff --git a/lib/public/Migration/SimpleMigrationStep.php b/lib/public/Migration/SimpleMigrationStep.php new file mode 100644 index 00000000000..df4ae4e2eb6 --- /dev/null +++ b/lib/public/Migration/SimpleMigrationStep.php @@ -0,0 +1,59 @@ +<?php +/** + * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Migration; + +use Doctrine\DBAL\Schema\Schema; + +/** + * @since 13.0.0 + */ +abstract class SimpleMigrationStep implements IMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @return null|Schema + * @since 13.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + return null; + } + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `Schema` + * @param array $options + * @since 13.0.0 + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + } +} |