Unverified Commit 3415c0e6 authored by Thomas Müller's avatar Thomas Müller
Browse files

Introduce own migration mechanism

Implement occ commands migrations:status, migrations:execute, migrations:generate and migrations:migrate
parent 491a7b1e
......@@ -22,42 +22,45 @@
namespace OC\Core\Command\Db\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand as DBALExecuteCommand;
use OC\DB\MigrationService;
use OCP\IConfig;
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 DBALExecuteCommand {
class ExecuteCommand extends Command {
/** @var Connection */
private $ocConnection;
/** @var IDBConnection */
private $connection;
/**
* @param \OCP\IConfig $config
* ExecuteCommand constructor.
*
* @param IDBConnection $connection
*/
public function __construct(IConfig $config, Connection $connection) {
$this->config = $config;
$this->ocConnection = $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
parent::__construct();
}
protected function configure() {
$this->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on');
$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();
$mc = $ms->buildConfiguration($appName, $this->ocConnection);
$this->setMigrationConfiguration($mc);
$ms = new MigrationService($appName, $this->connection);
$version = $input->getArgument('version');
parent::execute($input, $output);
$ms->executeStep($version);
}
}
......@@ -22,42 +22,144 @@
namespace OC\Core\Command\Db\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand as DBALGenerateCommand;
use OC\DB\MigrationService;
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 DBALGenerateCommand {
class GenerateCommand extends Command {
/** @var Connection */
private $ocConnection;
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 \OCP\IConfig $config
* @param IDBConnection $connection
*/
public function __construct(IConfig $config, Connection $connection) {
$this->config = $config;
$this->ocConnection = $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
parent::__construct();
}
protected function configure() {
$this->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on');
$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();
$mc = $ms->buildConfiguration($appName, $this->ocConnection);
$this->setMigrationConfiguration($mc);
$ms = new MigrationService($appName, $this->connection);
$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;
}
parent::execute($input, $output);
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');
}
}
......@@ -22,42 +22,42 @@
namespace OC\Core\Command\Db\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand as DBALMigrateCommand;
use OC\DB\MigrationService;
use OCP\IConfig;
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 DBALMigrateCommand {
class MigrateCommand extends Command {
/** @var Connection */
private $ocConnection;
/** @var IDBConnection */
private $connection;
/**
* @param \OCP\IConfig $config
* @param IDBConnection $connection
*/
public function __construct(IConfig $config, Connection $connection) {
$this->config = $config;
$this->ocConnection = $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
parent::__construct();
}
protected function configure() {
$this->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on');
$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();
$mc = $ms->buildConfiguration($appName, $this->ocConnection);
$this->setMigrationConfiguration($mc);
$ms = new MigrationService($appName, $this->connection);
$version = $input->getArgument('version');
parent::execute($input, $output);
$ms->migrate($version);
}
}
......@@ -21,43 +21,94 @@
namespace OC\Core\Command\Db\Migrations;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand as DBALStatusCommand;
use OC\DB\MigrationService;
use OCP\IConfig;
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 DBALStatusCommand {
class StatusCommand extends Command {
/** @var Connection */
private $ocConnection;
/** @var IDBConnection */
private $connection;
/**
* @param \OCP\IConfig $config
* @param IDBConnection $connection
*/
public function __construct(IConfig $config, Connection $connection) {
$this->config = $config;
$this->ocConnection = $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
parent::__construct();
}
protected function configure() {
parent::configure();
$this->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on');
$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();
$mc = $ms->buildConfiguration($appName, $this->ocConnection);
$this->setMigrationConfiguration($mc);
$ms = new MigrationService($appName, $this->connection);
$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));
parent::execute($input, $output);
$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;
}
}
......@@ -123,12 +123,4 @@ class Version20170111103310 extends AbstractMigration {
$table->addUniqueIndex(['mount_id', 'key'], 'option_mount_key');
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema) {
// this down() migration is auto-generated, please modify it to your needs
}
}
......@@ -82,10 +82,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()));
$application->add(new OC\Core\Command\Db\Migrations\StatusCommand(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
$application->add(new OC\Core\Command\Db\Migrations\MigrateCommand(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
$application->add(new OC\Core\Command\Db\Migrations\GenerateCommand(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
$application->add(new OC\Core\Command\Db\Migrations\ExecuteCommand(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()));
$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()));
......
......@@ -305,7 +305,6 @@ class Db implements IDb {
* Create the schema of the connected database
*
* @return Schema
* @since 10.0.0
*/
public function createSchema() {
return $this->connection->createSchema();
......
......@@ -21,56 +21,377 @@
namespace OC\DB;
use Doctrine\DBAL\Migrations\Configuration\Configuration;
use Doctrine\DBAL\Migrations\Migration;
use Doctrine\DBAL\Migrations\OutputWriter;
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;
/**
* @param string $appName
* MigrationService constructor.
*
* @param $appName
* @param IDBConnection $connection
* @return Configuration
* @param AppLocator $appLocator
* @param IOutput|null $output
* @throws \Exception
*/
public function buildConfiguration($appName, $connection) {
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') {
$migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
$migrationsNamespace = 'OC\\Migrations';
$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
$this->migrationsNamespace = 'OC\\Migrations';
} else {
$appPath = \OC_App::getAppPath($appName);
if (!$appPath) {
throw new \InvalidArgumentException('Path to app is not defined.');
if (is_null($appLocator)) {
$appLocator = new AppLocator();
}
$migrationsPath = "$appPath/appinfo/Migrations";
$migrationsNamespace = "OCA\\$appName\\Migrations";
$appPath = $appLocator->getAppPath($appName);
$this->migrationsPath = "$appPath/appinfo/Migrations";
$this->migrationsNamespace = "OCA\\$appName\\Migrations";
}
if (!is_dir($migrationsPath)) {
if (!mkdir($migrationsPath)) {
throw new \Exception("Could not create migration folder \"$migrationsPath\"");
if (!is_dir($this->migrationsPath)) {
if (!mkdir($this->migrationsPath)) {
throw new \Exception("Could not create migration folder \"{$this->migrationsPath}\"");
};
}
$prefix = $connection->getPrefix();
$mc = new MigrationConfiguration($connection);
$mc->setMigrationsDirectory($migrationsPath);
$mc->setMigrationsNamespace($migrationsNamespace);
$mc->setMigrationsTableName("{$prefix}{$appName}_migration_versions");
return $mc;
}
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
),