Fixme: - Install and update of apps - No revert on live systems (debug only) - Service adjustment to our interface - Loading via autoloader Signed-off-by: Joas Schilling <coding@schilljs.com>tags/v13.0.0beta1
@@ -0,0 +1,67 @@ | |||
<?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\Core\Command\Db\Migrations; | |||
use OC\DB\MigrationService; | |||
use OC\Migration\ConsoleOutput; | |||
use OCP\IDBConnection; | |||
use Symfony\Component\Console\Command\Command; | |||
use Symfony\Component\Console\Input\InputArgument; | |||
use Symfony\Component\Console\Input\InputInterface; | |||
use Symfony\Component\Console\Output\OutputInterface; | |||
class ExecuteCommand extends Command { | |||
/** @var IDBConnection */ | |||
private $connection; | |||
/** | |||
* ExecuteCommand constructor. | |||
* | |||
* @param IDBConnection $connection | |||
*/ | |||
public function __construct(IDBConnection $connection) { | |||
$this->connection = $connection; | |||
parent::__construct(); | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('migrations:execute') | |||
->setDescription('Execute a single migration version manually.') | |||
->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') | |||
->addArgument('version', InputArgument::REQUIRED, 'The version to execute.', null); | |||
parent::configure(); | |||
} | |||
public function execute(InputInterface $input, OutputInterface $output) { | |||
$appName = $input->getArgument('app'); | |||
$ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); | |||
$version = $input->getArgument('version'); | |||
$ms->executeStep($version); | |||
} | |||
} |
@@ -0,0 +1,166 @@ | |||
<?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\Core\Command\Db\Migrations; | |||
use OC\DB\MigrationService; | |||
use OC\Migration\ConsoleOutput; | |||
use OCP\IConfig; | |||
use OCP\IDBConnection; | |||
use Symfony\Component\Console\Command\Command; | |||
use Symfony\Component\Console\Exception\RuntimeException; | |||
use Symfony\Component\Console\Input\InputArgument; | |||
use Symfony\Component\Console\Input\InputInterface; | |||
use Symfony\Component\Console\Output\OutputInterface; | |||
class GenerateCommand extends Command { | |||
private static $_templateSimple = | |||
'<?php | |||
namespace <namespace>; | |||
use OCP\Migration\ISimpleMigration; | |||
use OCP\Migration\IOutput; | |||
/** | |||
* Auto-generated migration step: Please modify to your needs! | |||
*/ | |||
class Version<version> implements ISimpleMigration { | |||
/** | |||
* @param IOutput $out | |||
*/ | |||
public function run(IOutput $out) { | |||
// auto-generated - please modify it to your needs | |||
} | |||
} | |||
'; | |||
private static $_templateSchema = | |||
'<?php | |||
namespace <namespace>; | |||
use Doctrine\DBAL\Schema\Schema; | |||
use OCP\Migration\ISchemaMigration; | |||
/** | |||
* Auto-generated migration step: Please modify to your needs! | |||
*/ | |||
class Version<version> implements ISchemaMigration { | |||
public function changeSchema(Schema $schema, array $options) { | |||
// auto-generated - please modify it to your needs | |||
} | |||
} | |||
'; | |||
private static $_templateSql = | |||
'<?php | |||
namespace <namespace>; | |||
use OCP\IDBConnection; | |||
use OCP\Migration\ISqlMigration; | |||
/** | |||
* Auto-generated migration step: Please modify to your needs! | |||
*/ | |||
class Version<version> implements ISqlMigration { | |||
public function sql(IDBConnection $connection) { | |||
// auto-generated - please modify it to your needs | |||
} | |||
} | |||
'; | |||
/** @var IDBConnection */ | |||
private $connection; | |||
/** | |||
* @param IDBConnection $connection | |||
*/ | |||
public function __construct(IDBConnection $connection) { | |||
$this->connection = $connection; | |||
parent::__construct(); | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('migrations:generate') | |||
->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') | |||
->addArgument('kind', InputArgument::REQUIRED, 'simple, schema or sql - defines the kind of migration to be generated'); | |||
parent::configure(); | |||
} | |||
public function execute(InputInterface $input, OutputInterface $output) { | |||
$appName = $input->getArgument('app'); | |||
$ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); | |||
$kind = $input->getArgument('kind'); | |||
$version = date('YmdHis'); | |||
$path = $this->generateMigration($ms, $version, $kind); | |||
$output->writeln("New migration class has been generated to <info>$path</info>"); | |||
} | |||
/** | |||
* @param MigrationService $ms | |||
* @param string $version | |||
* @param string $kind | |||
* @return string | |||
*/ | |||
private function generateMigration(MigrationService $ms, $version, $kind) { | |||
$placeHolders = [ | |||
'<namespace>', | |||
'<version>', | |||
]; | |||
$replacements = [ | |||
$ms->getMigrationsNamespace(), | |||
$version, | |||
]; | |||
$code = str_replace($placeHolders, $replacements, $this->getTemplate($kind)); | |||
$dir = $ms->getMigrationsDirectory(); | |||
$path = $dir . '/Version' . $version . '.php'; | |||
if (file_put_contents($path, $code) === false) { | |||
throw new RuntimeException('Failed to generate new migration step.'); | |||
} | |||
return $path; | |||
} | |||
private function getTemplate($kind) { | |||
if ($kind === 'simple') { | |||
return self::$_templateSimple; | |||
} | |||
if ($kind === 'schema') { | |||
return self::$_templateSchema; | |||
} | |||
if ($kind === 'sql') { | |||
return self::$_templateSql; | |||
} | |||
throw new \InvalidArgumentException('Kind can only be one of the following: simple, schema or sql'); | |||
} | |||
} |
@@ -0,0 +1,64 @@ | |||
<?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\Core\Command\Db\Migrations; | |||
use OC\DB\MigrationService; | |||
use OC\Migration\ConsoleOutput; | |||
use OCP\IDBConnection; | |||
use Symfony\Component\Console\Command\Command; | |||
use Symfony\Component\Console\Input\InputArgument; | |||
use Symfony\Component\Console\Input\InputInterface; | |||
use Symfony\Component\Console\Output\OutputInterface; | |||
class MigrateCommand extends Command { | |||
/** @var IDBConnection */ | |||
private $connection; | |||
/** | |||
* @param IDBConnection $connection | |||
*/ | |||
public function __construct(IDBConnection $connection) { | |||
$this->connection = $connection; | |||
parent::__construct(); | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('migrations:migrate') | |||
->setDescription('Execute a migration to a specified version or the latest available version.') | |||
->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on') | |||
->addArgument('version', InputArgument::OPTIONAL, 'The version number (YYYYMMDDHHMMSS) or alias (first, prev, next, latest) to migrate to.', 'latest'); | |||
parent::configure(); | |||
} | |||
public function execute(InputInterface $input, OutputInterface $output) { | |||
$appName = $input->getArgument('app'); | |||
$ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); | |||
$version = $input->getArgument('version'); | |||
$ms->migrate($version); | |||
} | |||
} |
@@ -0,0 +1,115 @@ | |||
<?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\Core\Command\Db\Migrations; | |||
use OC\DB\MigrationService; | |||
use OC\Migration\ConsoleOutput; | |||
use OCP\IDBConnection; | |||
use Symfony\Component\Console\Command\Command; | |||
use Symfony\Component\Console\Input\InputArgument; | |||
use Symfony\Component\Console\Input\InputInterface; | |||
use Symfony\Component\Console\Output\OutputInterface; | |||
class StatusCommand extends Command { | |||
/** @var IDBConnection */ | |||
private $connection; | |||
/** | |||
* @param IDBConnection $connection | |||
*/ | |||
public function __construct(IDBConnection $connection) { | |||
$this->connection = $connection; | |||
parent::__construct(); | |||
} | |||
protected function configure() { | |||
$this | |||
->setName('migrations:status') | |||
->setDescription('View the status of a set of migrations.') | |||
->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on'); | |||
} | |||
public function execute(InputInterface $input, OutputInterface $output) { | |||
$appName = $input->getArgument('app'); | |||
$ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output)); | |||
$infos = $this->getMigrationsInfos($ms); | |||
foreach ($infos as $key => $value) { | |||
$output->writeln(" <comment>>></comment> $key: " . str_repeat(' ', 50 - strlen($key)) . $value); | |||
} | |||
} | |||
/** | |||
* @param MigrationService $ms | |||
* @return array associative array of human readable info name as key and the actual information as value | |||
*/ | |||
public function getMigrationsInfos(MigrationService $ms) { | |||
$executedMigrations = $ms->getMigratedVersions(); | |||
$availableMigrations = $ms->getAvailableVersions(); | |||
$executedUnavailableMigrations = array_diff($executedMigrations, array_keys($availableMigrations)); | |||
$numExecutedUnavailableMigrations = count($executedUnavailableMigrations); | |||
$numNewMigrations = count(array_diff(array_keys($availableMigrations), $executedMigrations)); | |||
$infos = [ | |||
'App' => $ms->getApp(), | |||
'Version Table Name' => $ms->getMigrationsTableName(), | |||
'Migrations Namespace' => $ms->getMigrationsNamespace(), | |||
'Migrations Directory' => $ms->getMigrationsDirectory(), | |||
'Previous Version' => $this->getFormattedVersionAlias($ms, 'prev'), | |||
'Current Version' => $this->getFormattedVersionAlias($ms, 'current'), | |||
'Next Version' => $this->getFormattedVersionAlias($ms, 'next'), | |||
'Latest Version' => $this->getFormattedVersionAlias($ms, 'latest'), | |||
'Executed Migrations' => count($executedMigrations), | |||
'Executed Unavailable Migrations' => $numExecutedUnavailableMigrations, | |||
'Available Migrations' => count($availableMigrations), | |||
'New Migrations' => $numNewMigrations, | |||
]; | |||
return $infos; | |||
} | |||
/** | |||
* @param MigrationService $migrationService | |||
* @param string $alias | |||
* @return mixed|null|string | |||
*/ | |||
private function getFormattedVersionAlias(MigrationService $migrationService, $alias) { | |||
$migration = $migrationService->getMigration($alias); | |||
//No version found | |||
if ($migration === null) { | |||
if ($alias === 'next') { | |||
return 'Already at latest migration step'; | |||
} | |||
if ($alias === 'prev') { | |||
return 'Already at first migration step'; | |||
} | |||
} | |||
return $migration; | |||
} | |||
} |
@@ -85,6 +85,10 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) { | |||
$application->add(new OC\Core\Command\Db\GenerateChangeScript()); | |||
$application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory(\OC::$server->getSystemConfig()))); | |||
$application->add(new OC\Core\Command\Db\ConvertMysqlToMB4(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection(), \OC::$server->getURLGenerator(), \OC::$server->getLogger())); | |||
$application->add(new OC\Core\Command\Db\Migrations\StatusCommand(\OC::$server->getDatabaseConnection())); | |||
$application->add(new OC\Core\Command\Db\Migrations\MigrateCommand(\OC::$server->getDatabaseConnection())); | |||
$application->add(new OC\Core\Command\Db\Migrations\GenerateCommand(\OC::$server->getDatabaseConnection())); | |||
$application->add(new OC\Core\Command\Db\Migrations\ExecuteCommand(\OC::$server->getDatabaseConnection())); | |||
$application->add(new OC\Core\Command\Encryption\Disable(\OC::$server->getConfig())); | |||
$application->add(new OC\Core\Command\Encryption\Enable(\OC::$server->getConfig(), \OC::$server->getEncryptionManager())); |
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,401 @@ | |||
<?php | |||
/** | |||
* @author Thomas Müller <thomas.mueller@tmit.eu> | |||
* @author Vincent Petry <pvince81@owncloud.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 OC\IntegrityCheck\Helpers\AppLocator; | |||
use OC\Migration\SimpleOutput; | |||
use OCP\AppFramework\QueryException; | |||
use OCP\IDBConnection; | |||
use OCP\Migration\IOutput; | |||
use OCP\Migration\ISchemaMigration; | |||
use OCP\Migration\ISimpleMigration; | |||
use OCP\Migration\ISqlMigration; | |||
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 | |||
*/ | |||
function __construct($appName, IDBConnection $connection, IOutput $output = null, AppLocator $appLocator = null) { | |||
$this->appName = $appName; | |||
$this->connection = $connection; | |||
$this->output = $output; | |||
if (is_null($this->output)) { | |||
$this->output = new SimpleOutput(\OC::$server->getLogger(), $appName); | |||
} | |||
if ($appName === 'core') { | |||
$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations'; | |||
$this->migrationsNamespace = 'OC\\Migrations'; | |||
} else { | |||
if (is_null($appLocator)) { | |||
$appLocator = new AppLocator(); | |||
} | |||
$appPath = $appLocator->getAppPath($appName); | |||
$this->migrationsPath = "$appPath/appinfo/Migrations"; | |||
$this->migrationsNamespace = "OCA\\$appName\\Migrations"; | |||
} | |||
if (!is_dir($this->migrationsPath)) { | |||
if (!mkdir($this->migrationsPath)) { | |||
throw new \Exception("Could not create migration folder \"{$this->migrationsPath}\""); | |||
}; | |||
} | |||
} | |||
private static function requireOnce($file) { | |||
require_once $file; | |||
} | |||
/** | |||
* 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_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) { | |||
return (basename($a) < basename($b)) ? -1 : 1; | |||
}); | |||
$migrations = []; | |||
foreach ($files as $file) { | |||
static::requireOnce($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 | |||
*/ | |||
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[] $knownMigrations | |||
*/ | |||
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); | |||
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)); | |||
} | |||
/** | |||
* @return string | |||
*/ | |||
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 | |||
*/ | |||
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 \Exception | |||
*/ | |||
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 \Exception("Migration step '$class' is unknown"); | |||
} | |||
} | |||
return $s; | |||
} | |||
/** | |||
* Executes one explicit version | |||
* | |||
* @param string $version | |||
*/ | |||
public function executeStep($version) { | |||
// FIXME our interface | |||
$instance = $this->createInstance($version); | |||
if ($instance instanceof ISimpleMigration) { | |||
$instance->run($this->output); | |||
} | |||
if ($instance instanceof ISqlMigration) { | |||
$sqls = $instance->sql($this->connection); | |||
foreach ($sqls as $s) { | |||
$this->connection->executeQuery($s); | |||
} | |||
} | |||
if ($instance instanceof ISchemaMigration) { | |||
$toSchema = $this->connection->createSchema(); | |||
$instance->changeSchema($toSchema, ['tablePrefix' => $this->connection->getPrefix()]); | |||
$this->connection->migrateToSchema($toSchema); | |||
} | |||
$this->markAsExecuted($version); | |||
} | |||
private function ensureMigrationsAreLoaded() { | |||
if (empty($this->migrations)) { | |||
$this->migrations = $this->findMigrations(); | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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); | |||
} | |||
/** |
@@ -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) { |
@@ -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() { | |||
} | |||
} |
@@ -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(), |
@@ -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(); | |||
} | |||
} |
@@ -32,6 +32,7 @@ | |||
namespace OC; | |||
use OC\DB\MigrationService; | |||
use OC\Hooks\BasicEmitter; | |||
use OC\IntegrityCheck\Checker; | |||
use OC_App; | |||
@@ -300,8 +301,11 @@ 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 | |||
if (is_dir(\OC::$SERVERROOT . '/core/Migrations')) { | |||
$ms = new MigrationService('core', \OC::$server->getDatabaseConnection()); | |||
$ms->migrate(); | |||
} | |||
$this->emit('\OC\Updater', 'dbUpgrade'); | |||
} |
@@ -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; | |||
@@ -1043,12 +1044,18 @@ class OC_App { | |||
} | |||
$appData = self::getAppInfo($appId); | |||
self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); | |||
if (file_exists($appPath . '/appinfo/database.xml')) { | |||
if (isset($appData['use-migrations']) && $appData['use-migrations'] === 'true') { | |||
$ms = new MigrationService($appId, \OC::$server->getDatabaseConnection()); | |||
$ms->migrate(); | |||
} else if (file_exists($appPath . '/appinfo/database.xml')) { | |||
OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml'); | |||
} | |||
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); |
@@ -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); | |||
} |
@@ -0,0 +1,49 @@ | |||
<?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 | |||
* @since 13.0.0 | |||
*/ | |||
public function preSchemaChange(IOutput $output); | |||
/** | |||
* @param Schema $schema | |||
* @param array $options | |||
* @since 13.0.0 | |||
*/ | |||
public function changeSchema(Schema $schema, array $options); | |||
/** | |||
* @param IOutput $output | |||
* @since 13.0.0 | |||
*/ | |||
public function postSchemaChange(IOutput $output); | |||
} |
@@ -0,0 +1,162 @@ | |||
<?php | |||
/** | |||
* Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu> | |||
* 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 OC\DB\Connection; | |||
use OC\DB\MigrationService; | |||
use OCP\IDBConnection; | |||
use OCP\Migration\ISchemaMigration; | |||
use OCP\Migration\ISqlMigration; | |||
/** | |||
* Class MigrationsTest | |||
* | |||
* @package Test\DB | |||
*/ | |||
class MigrationsTest extends \Test\TestCase { | |||
/** @var MigrationService | \PHPUnit_Framework_MockObject_MockObject */ | |||
private $migrationService; | |||
/** @var \PHPUnit_Framework_MockObject_MockObject | IDBConnection $db */ | |||
private $db; | |||
public function setUp() { | |||
parent::setUp(); | |||
$this->db = $this->createMock(Connection::class); | |||
$this->db->expects($this->any())->method('getPrefix')->willReturn('test_oc_'); | |||
$this->migrationService = new MigrationService('testing', $this->db); | |||
} | |||
public function testGetters() { | |||
$this->assertEquals('testing', $this->migrationService->getApp()); | |||
$this->assertEquals(\OC::$SERVERROOT . '/apps/testing/appinfo/Migrations', $this->migrationService->getMigrationsDirectory()); | |||
$this->assertEquals('OCA\testing\Migrations', $this->migrationService->getMigrationsNamespace()); | |||
$this->assertEquals('test_oc_migrations', $this->migrationService->getMigrationsTableName()); | |||
} | |||
public function testCore() { | |||
$this->migrationService = new MigrationService('core', $this->db); | |||
$this->assertEquals('core', $this->migrationService->getApp()); | |||
$this->assertEquals(\OC::$SERVERROOT . '/core/Migrations', $this->migrationService->getMigrationsDirectory()); | |||
$this->assertEquals('OC\Migrations', $this->migrationService->getMigrationsNamespace()); | |||
$this->assertEquals('test_oc_migrations', $this->migrationService->getMigrationsTableName()); | |||
} | |||
/** | |||
* @expectedException \InvalidArgumentException | |||
* @expectedExceptionMessage Version 20170130180000 is unknown. | |||
*/ | |||
public function testExecuteUnknownStep() { | |||
$this->migrationService->executeStep('20170130180000'); | |||
} | |||
/** | |||
* @expectedException \Exception | |||
* @expectedExceptionMessage App not found | |||
*/ | |||
public function testUnknownApp() { | |||
$migrationService = new MigrationService('unknown-bloody-app', $this->db); | |||
} | |||
/** | |||
* @expectedException \Exception | |||
* @expectedExceptionMessage Migration step 'X' is unknown | |||
*/ | |||
public function testExecuteStepWithUnknownClass() { | |||
$this->migrationService = $this->getMockBuilder(MigrationService::class) | |||
->setMethods(['findMigrations']) | |||
->setConstructorArgs(['testing', $this->db]) | |||
->getMock(); | |||
$this->migrationService->expects($this->any())->method('findMigrations')->willReturn( | |||
['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] | |||
); | |||
$this->migrationService->executeStep('20170130180000'); | |||
} | |||
public function testExecuteStepWithSchemaMigrationStep() { | |||
$schema = $this->createMock(Schema::class); | |||
$this->db->expects($this->any())->method('createSchema')->willReturn($schema); | |||
$step = $this->createMock(ISchemaMigration::class); | |||
$step->expects($this->once())->method('changeSchema'); | |||
$this->migrationService = $this->getMockBuilder(MigrationService::class) | |||
->setMethods(['createInstance']) | |||
->setConstructorArgs(['testing', $this->db]) | |||
->getMock(); | |||
$this->migrationService->expects($this->any())->method('createInstance')->with('20170130180000')->willReturn($step); | |||
$this->migrationService->executeStep('20170130180000'); | |||
} | |||
public function testExecuteStepWithSqlMigrationStep() { | |||
$this->db->expects($this->exactly(3))->method('executeQuery')->withConsecutive(['1'], ['2'], ['3']); | |||
$step = $this->createMock(ISqlMigration::class); | |||
$step->expects($this->once())->method('sql')->willReturn(['1', '2', '3']); | |||
$this->migrationService = $this->getMockBuilder(MigrationService::class) | |||
->setMethods(['createInstance']) | |||
->setConstructorArgs(['testing', $this->db]) | |||
->getMock(); | |||
$this->migrationService->expects($this->any())->method('createInstance')->with('20170130180000')->willReturn($step); | |||
$this->migrationService->executeStep('20170130180000'); | |||
} | |||
public function testGetMigration() { | |||
$this->migrationService = $this->getMockBuilder(MigrationService::class) | |||
->setMethods(['getMigratedVersions', 'findMigrations']) | |||
->setConstructorArgs(['testing', $this->db]) | |||
->getMock(); | |||
$this->migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( | |||
['20170130180000', '20170130180001'] | |||
); | |||
$this->migrationService->expects($this->any())->method('findMigrations')->willReturn( | |||
['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] | |||
); | |||
$this->assertEquals( | |||
['20170130180000', '20170130180001', '20170130180002', '20170130180003'], | |||
$this->migrationService->getAvailableVersions()); | |||
$migration = $this->migrationService->getMigration('current'); | |||
$this->assertEquals('20170130180001', $migration); | |||
$migration = $this->migrationService->getMigration('prev'); | |||
$this->assertEquals('20170130180000', $migration); | |||
$migration = $this->migrationService->getMigration('next'); | |||
$this->assertEquals('20170130180002', $migration); | |||
$migration = $this->migrationService->getMigration('latest'); | |||
$this->assertEquals('20170130180003', $migration); | |||
} | |||
public function testMigrate() { | |||
$this->migrationService = $this->getMockBuilder(MigrationService::class) | |||
->setMethods(['getMigratedVersions', 'findMigrations', 'executeStep']) | |||
->setConstructorArgs(['testing', $this->db]) | |||
->getMock(); | |||
$this->migrationService->expects($this->any())->method('getMigratedVersions')->willReturn( | |||
['20170130180000', '20170130180001'] | |||
); | |||
$this->migrationService->expects($this->any())->method('findMigrations')->willReturn( | |||
['20170130180000' => 'X', '20170130180001' => 'Y', '20170130180002' => 'Z', '20170130180003' => 'A'] | |||
); | |||
$this->assertEquals( | |||
['20170130180000', '20170130180001', '20170130180002', '20170130180003'], | |||
$this->migrationService->getAvailableVersions()); | |||
$this->migrationService->expects($this->exactly(2))->method('executeStep') | |||
->withConsecutive(['20170130180002'], ['20170130180003']); | |||
$this->migrationService->migrate(); | |||
} | |||
} |