2013-12-07 17:44:23 +04:00
< ? php
/**
2016-07-21 18:07:57 +03:00
* @ copyright Copyright ( c ) 2016 , ownCloud , Inc .
*
2015-03-26 13:44:34 +03:00
* @ author Andreas Fischer < bantu @ owncloud . com >
* @ author Bart Visscher < bartv @ thisnet . nl >
2016-07-21 18:07:57 +03:00
* @ author Joas Schilling < coding @ schilljs . com >
2017-11-06 17:56:42 +03:00
* @ author Lukas Reschke < lukas @ statuscode . ch >
2015-03-26 13:44:34 +03:00
* @ author Morris Jobke < hey @ morrisjobke . de >
2017-11-06 17:56:42 +03:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
* @ author Sander Ruitenbeek < sander @ grids . be >
2015-03-26 13:44:34 +03:00
* @ author tbelau666 < thomas . belau @ gmx . de >
* @ author Thomas Müller < thomas . mueller @ tmit . eu >
*
* @ 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 />
2014-03-31 19:06:02 +04:00
*
2013-12-07 17:44:23 +04:00
*/
2015-02-26 13:37:37 +03:00
2013-12-07 17:44:23 +04:00
namespace OC\Core\Command\Db ;
2017-07-19 14:15:32 +03:00
use Doctrine\DBAL\DBALException ;
use Doctrine\DBAL\Schema\Table ;
2017-07-19 17:17:46 +03:00
use Doctrine\DBAL\Types\Type ;
2017-07-19 12:48:58 +03:00
use OC\DB\MigrationService ;
2017-03-26 18:11:57 +03:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2014-11-19 02:25:26 +03:00
use \OCP\IConfig ;
2014-03-31 23:07:48 +04:00
use OC\DB\Connection ;
use OC\DB\ConnectionFactory ;
2016-09-21 15:21:39 +03:00
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface ;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext ;
2013-12-07 17:44:23 +04:00
use Symfony\Component\Console\Command\Command ;
2016-09-06 22:18:51 +03:00
use Symfony\Component\Console\Helper\ProgressBar ;
use Symfony\Component\Console\Helper\QuestionHelper ;
2013-12-07 17:44:23 +04:00
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
2016-09-06 22:18:51 +03:00
use Symfony\Component\Console\Question\ConfirmationQuestion ;
use Symfony\Component\Console\Question\Question ;
2013-12-07 17:44:23 +04:00
2016-09-21 15:21:39 +03:00
class ConvertType extends Command implements CompletionAwareInterface {
2013-12-24 16:36:32 +04:00
/**
2014-11-19 02:25:26 +03:00
* @ var \OCP\IConfig
2013-12-24 16:36:32 +04:00
*/
protected $config ;
2014-03-31 22:00:44 +04:00
/**
* @ var \OC\DB\ConnectionFactory
*/
protected $connectionFactory ;
2017-03-26 18:11:57 +03:00
/** @var array */
protected $columnTypes ;
2013-12-24 16:36:32 +04:00
/**
2014-11-19 02:25:26 +03:00
* @ param \OCP\IConfig $config
2014-03-31 22:00:44 +04:00
* @ param \OC\DB\ConnectionFactory $connectionFactory
2013-12-24 16:36:32 +04:00
*/
2014-11-19 02:25:26 +03:00
public function __construct ( IConfig $config , ConnectionFactory $connectionFactory ) {
2013-12-24 16:36:32 +04:00
$this -> config = $config ;
2014-03-31 22:00:44 +04:00
$this -> connectionFactory = $connectionFactory ;
2013-12-24 16:36:32 +04:00
parent :: __construct ();
}
2013-12-07 17:44:23 +04:00
protected function configure () {
$this
2014-02-11 21:01:41 +04:00
-> setName ( 'db:convert-type' )
2016-07-30 16:39:32 +03:00
-> setDescription ( 'Convert the Nextcloud database to the newly configured one' )
2013-12-07 17:44:23 +04:00
-> addArgument (
'type' ,
InputArgument :: REQUIRED ,
'the type of the database to convert to'
)
-> addArgument (
'username' ,
InputArgument :: REQUIRED ,
'the username of the database to convert to'
)
-> addArgument (
'hostname' ,
InputArgument :: REQUIRED ,
'the hostname of the database to convert to'
)
-> addArgument (
'database' ,
InputArgument :: REQUIRED ,
'the name of the database to convert to'
)
-> addOption (
'port' ,
null ,
InputOption :: VALUE_REQUIRED ,
'the port of the database to convert to'
)
-> addOption (
'password' ,
null ,
InputOption :: VALUE_REQUIRED ,
2014-04-15 19:30:43 +04:00
'the password of the database to convert to. Will be asked when not specified. Can also be passed via stdin.'
2013-12-07 17:44:23 +04:00
)
2014-01-11 15:23:28 +04:00
-> addOption (
'clear-schema' ,
null ,
InputOption :: VALUE_NONE ,
'remove all tables from the destination database'
)
2014-04-09 18:42:22 +04:00
-> addOption (
'all-apps' ,
null ,
InputOption :: VALUE_NONE ,
'whether to create schema for all apps instead of only installed apps'
)
2016-05-13 11:04:10 +03:00
-> addOption (
'chunk-size' ,
null ,
InputOption :: VALUE_REQUIRED ,
'the maximum number of database rows to handle in a single query, bigger tables will be handled in chunks of this size. Lower this if the process runs out of memory during conversion.' ,
1000
)
2013-12-07 17:44:23 +04:00
;
}
2014-04-15 19:19:47 +04:00
protected function validateInput ( InputInterface $input , OutputInterface $output ) {
2014-05-09 14:31:08 +04:00
$type = $this -> connectionFactory -> normalizeType ( $input -> getArgument ( 'type' ));
if ( $type === 'sqlite3' ) {
2014-04-15 20:14:26 +04:00
throw new \InvalidArgumentException (
'Converting to SQLite (sqlite3) is currently not supported.'
);
2014-04-15 19:14:26 +04:00
}
2014-11-19 02:25:26 +03:00
if ( $type === $this -> config -> getSystemValue ( 'dbtype' , '' )) {
2014-04-15 20:14:26 +04:00
throw new \InvalidArgumentException ( sprintf (
'Can not convert from %1$s to %1$s.' ,
2014-04-15 19:05:31 +04:00
$type
2014-04-09 17:57:33 +04:00
));
}
2014-04-15 19:19:47 +04:00
if ( $type === 'oci' && $input -> getOption ( 'clear-schema' )) {
// Doctrine unconditionally tries (at least in version 2.3)
// to drop sequence triggers when dropping a table, even though
// such triggers may not exist. This results in errors like
// "ORA-04080: trigger 'OC_STORAGES_AI_PK' does not exist".
2014-04-15 20:14:26 +04:00
throw new \InvalidArgumentException (
'The --clear-schema option is not supported when converting to Oracle (oci).'
);
2014-04-15 19:19:47 +04:00
}
}
2014-04-15 19:30:43 +04:00
protected function readPassword ( InputInterface $input , OutputInterface $output ) {
// Explicitly specified password
if ( $input -> getOption ( 'password' )) {
return ;
}
2014-04-15 20:04:54 +04:00
// Read from stdin. stream_set_blocking is used to prevent blocking
// when nothing is passed via stdin.
stream_set_blocking ( STDIN , 0 );
2014-04-15 19:30:43 +04:00
$password = file_get_contents ( 'php://stdin' );
2014-04-15 20:04:54 +04:00
stream_set_blocking ( STDIN , 1 );
2014-04-15 19:30:43 +04:00
if ( trim ( $password ) !== '' ) {
$input -> setOption ( 'password' , $password );
return ;
}
// Read password by interacting
if ( $input -> isInteractive ()) {
2016-09-06 22:18:51 +03:00
/** @var QuestionHelper $helper */
$helper = $this -> getHelper ( 'question' );
$question = new Question ( 'What is the database password?' );
$question -> setHidden ( true );
$question -> setHiddenFallback ( false );
$password = $helper -> ask ( $input , $output , $question );
2014-04-15 19:30:43 +04:00
$input -> setOption ( 'password' , $password );
return ;
}
}
2014-04-15 19:19:47 +04:00
protected function execute ( InputInterface $input , OutputInterface $output ) {
2014-04-15 20:14:26 +04:00
$this -> validateInput ( $input , $output );
2014-04-15 19:30:43 +04:00
$this -> readPassword ( $input , $output );
2016-01-07 12:26:00 +03:00
$fromDB = \OC :: $server -> getDatabaseConnection ();
2014-02-17 21:02:58 +04:00
$toDB = $this -> getToDBConnection ( $input , $output );
if ( $input -> getOption ( 'clear-schema' )) {
2014-04-15 20:20:36 +04:00
$this -> clearSchema ( $toDB , $input , $output );
2014-02-17 21:02:58 +04:00
}
2017-07-19 14:18:10 +03:00
$this -> createSchema ( $fromDB , $toDB , $input , $output );
2014-02-17 21:02:58 +04:00
$toTables = $this -> getTables ( $toDB );
$fromTables = $this -> getTables ( $fromDB );
2014-03-31 21:27:18 +04:00
2014-02-17 21:02:58 +04:00
// warn/fail if there are more tables in 'from' database
2014-04-11 23:46:02 +04:00
$extraFromTables = array_diff ( $fromTables , $toTables );
if ( ! empty ( $extraFromTables )) {
2014-04-11 23:40:45 +04:00
$output -> writeln ( '<comment>The following tables will not be converted:</comment>' );
2014-04-11 23:46:02 +04:00
$output -> writeln ( $extraFromTables );
2014-04-11 23:40:45 +04:00
if ( ! $input -> getOption ( 'all-apps' )) {
$output -> writeln ( '<comment>Please note that tables belonging to available but currently not installed apps</comment>' );
$output -> writeln ( '<comment>can be included by specifying the --all-apps option.</comment>' );
}
2016-09-06 22:18:51 +03:00
/** @var QuestionHelper $helper */
$helper = $this -> getHelper ( 'question' );
$question = new ConfirmationQuestion ( 'Continue with the conversion (y/n)? [n] ' , false );
if ( ! $helper -> ask ( $input , $output , $question )) {
2014-02-17 21:02:58 +04:00
return ;
}
}
2014-04-11 23:46:02 +04:00
$intersectingTables = array_intersect ( $toTables , $fromTables );
$this -> convertDB ( $fromDB , $toDB , $intersectingTables , $input , $output );
2014-02-17 21:02:58 +04:00
}
2017-07-19 14:18:10 +03:00
protected function createSchema ( Connection $fromDB , Connection $toDB , InputInterface $input , OutputInterface $output ) {
2014-04-09 18:46:21 +04:00
$output -> writeln ( '<info>Creating schema in new database</info>' );
2017-07-19 12:48:58 +03:00
2017-07-19 14:18:10 +03:00
$fromMS = new MigrationService ( 'core' , $fromDB );
$currentMigration = $fromMS -> getMigration ( 'current' );
if ( $currentMigration !== '0' ) {
$toMS = new MigrationService ( 'core' , $toDB );
$toMS -> migrate ( $currentMigration );
}
2017-07-19 12:48:58 +03:00
2014-04-09 18:46:21 +04:00
$schemaManager = new \OC\DB\MDB2SchemaManager ( $toDB );
$apps = $input -> getOption ( 'all-apps' ) ? \OC_App :: getAllApps () : \OC_App :: getEnabledApps ();
foreach ( $apps as $app ) {
if ( file_exists ( \OC_App :: getAppPath ( $app ) . '/appinfo/database.xml' )) {
$schemaManager -> createDbFromStructure ( \OC_App :: getAppPath ( $app ) . '/appinfo/database.xml' );
2017-07-19 12:48:58 +03:00
} else {
2017-07-19 14:18:10 +03:00
// Make sure autoloading works...
\OC_App :: loadApp ( $app );
$fromMS = new MigrationService ( $app , $fromDB );
$currentMigration = $fromMS -> getMigration ( 'current' );
if ( $currentMigration !== '0' ) {
$toMS = new MigrationService ( $app , $toDB );
2018-07-19 16:32:36 +03:00
$toMS -> migrate ( $currentMigration , true );
2017-07-19 14:18:10 +03:00
}
2014-04-09 18:46:21 +04:00
}
}
}
2014-04-09 17:21:57 +04:00
protected function getToDBConnection ( InputInterface $input , OutputInterface $output ) {
2013-12-07 17:44:23 +04:00
$type = $input -> getArgument ( 'type' );
2017-07-19 13:13:23 +03:00
$connectionParams = $this -> connectionFactory -> createConnectionParams ();
$connectionParams = array_merge ( $connectionParams , [
2014-03-31 22:00:44 +04:00
'host' => $input -> getArgument ( 'hostname' ),
'user' => $input -> getArgument ( 'username' ),
'password' => $input -> getOption ( 'password' ),
'dbname' => $input -> getArgument ( 'database' ),
2017-07-19 13:13:23 +03:00
]);
2013-12-07 17:44:23 +04:00
if ( $input -> getOption ( 'port' )) {
$connectionParams [ 'port' ] = $input -> getOption ( 'port' );
}
2014-03-31 22:00:44 +04:00
return $this -> connectionFactory -> getConnection ( $type , $connectionParams );
2014-02-17 21:02:58 +04:00
}
2013-12-07 17:44:23 +04:00
2014-04-15 20:20:36 +04:00
protected function clearSchema ( Connection $db , InputInterface $input , OutputInterface $output ) {
$toTables = $this -> getTables ( $db );
2014-04-09 17:45:30 +04:00
if ( ! empty ( $toTables )) {
$output -> writeln ( '<info>Clearing schema in new database</info>' );
}
foreach ( $toTables as $table ) {
2014-04-15 20:20:36 +04:00
$db -> getSchemaManager () -> dropTable ( $table );
2014-04-09 17:45:30 +04:00
}
}
2014-04-09 17:21:57 +04:00
protected function getTables ( Connection $db ) {
2015-01-12 15:57:46 +03:00
$filterExpression = '/^' . preg_quote ( $this -> config -> getSystemValue ( 'dbtableprefix' , 'oc_' )) . '/' ;
2014-12-01 01:17:09 +03:00
$db -> getConfiguration () ->
2015-01-12 15:57:46 +03:00
setFilterSchemaAssetsExpression ( $filterExpression );
2014-04-15 20:20:36 +04:00
return $db -> getSchemaManager () -> listTableNames ();
2014-02-17 21:02:58 +04:00
}
2014-01-11 15:23:28 +04:00
2017-07-20 23:48:13 +03:00
/**
* @ param Connection $fromDB
* @ param Connection $toDB
2017-07-19 14:15:32 +03:00
* @ param Table $table
2017-07-20 23:48:13 +03:00
* @ param InputInterface $input
* @ param OutputInterface $output
* @ suppress SqlInjectionChecker
*/
2017-07-19 14:15:32 +03:00
protected function copyTable ( Connection $fromDB , Connection $toDB , Table $table , InputInterface $input , OutputInterface $output ) {
if ( $table -> getName () === $toDB -> getPrefix () . 'migrations' ) {
2017-07-19 13:12:38 +03:00
$output -> writeln ( '<comment>Skipping migrations table because it was already filled by running the migrations</comment>' );
return ;
}
2016-05-13 11:04:10 +03:00
$chunkSize = $input -> getOption ( 'chunk-size' );
$query = $fromDB -> getQueryBuilder ();
$query -> automaticTablePrefix ( false );
$query -> selectAlias ( $query -> createFunction ( 'COUNT(*)' ), 'num_entries' )
2017-07-19 14:15:32 +03:00
-> from ( $table -> getName ());
2016-05-13 11:04:10 +03:00
$result = $query -> execute ();
$count = $result -> fetchColumn ();
$result -> closeCursor ();
$numChunks = ceil ( $count / $chunkSize );
if ( $numChunks > 1 ) {
$output -> writeln ( 'chunked query, ' . $numChunks . ' chunks' );
}
2016-09-06 22:18:51 +03:00
$progress = new ProgressBar ( $output , $count );
$progress -> start ();
2016-05-13 11:04:10 +03:00
$redraw = $count > $chunkSize ? 100 : ( $count > 100 ? 5 : 1 );
$progress -> setRedrawFrequency ( $redraw );
$query = $fromDB -> getQueryBuilder ();
$query -> automaticTablePrefix ( false );
$query -> select ( '*' )
2017-07-19 14:15:32 +03:00
-> from ( $table -> getName ())
2016-05-13 11:04:10 +03:00
-> setMaxResults ( $chunkSize );
2017-07-19 14:15:32 +03:00
try {
$orderColumns = $table -> getPrimaryKeyColumns ();
} catch ( DBALException $e ) {
$orderColumns = [];
2017-07-19 14:20:24 +03:00
foreach ( $table -> getColumns () as $column ) {
$orderColumns [] = $column -> getName ();
2017-07-19 14:15:32 +03:00
}
}
2017-07-19 14:20:24 +03:00
foreach ( $orderColumns as $column ) {
$query -> addOrderBy ( $column );
2017-07-19 14:15:32 +03:00
}
2016-05-13 11:04:10 +03:00
$insertQuery = $toDB -> getQueryBuilder ();
$insertQuery -> automaticTablePrefix ( false );
2017-07-19 14:15:32 +03:00
$insertQuery -> insert ( $table -> getName ());
2016-05-13 11:04:10 +03:00
$parametersCreated = false ;
for ( $chunk = 0 ; $chunk < $numChunks ; $chunk ++ ) {
$query -> setFirstResult ( $chunk * $chunkSize );
$result = $query -> execute ();
while ( $row = $result -> fetch ()) {
$progress -> advance ();
if ( ! $parametersCreated ) {
foreach ( $row as $key => $value ) {
$insertQuery -> setValue ( $key , $insertQuery -> createParameter ( $key ));
}
$parametersCreated = true ;
}
foreach ( $row as $key => $value ) {
2017-07-19 17:17:46 +03:00
$type = $this -> getColumnType ( $table , $key );
2017-03-26 23:43:19 +03:00
if ( $type !== false ) {
$insertQuery -> setParameter ( $key , $value , $type );
} else {
$insertQuery -> setParameter ( $key , $value );
}
2014-04-14 19:52:34 +04:00
}
2016-05-13 11:04:10 +03:00
$insertQuery -> execute ();
2013-12-07 17:44:23 +04:00
}
2016-05-13 11:04:10 +03:00
$result -> closeCursor ();
2013-12-07 17:44:23 +04:00
}
2014-02-17 21:02:58 +04:00
$progress -> finish ();
}
2013-12-07 17:44:23 +04:00
2017-07-19 17:17:46 +03:00
protected function getColumnType ( Table $table , $columnName ) {
$tableName = $table -> getName ();
if ( isset ( $this -> columnTypes [ $tableName ][ $columnName ])) {
return $this -> columnTypes [ $tableName ][ $columnName ];
2017-03-26 18:11:57 +03:00
}
2017-03-26 23:43:19 +03:00
2017-07-19 17:17:46 +03:00
$type = $table -> getColumn ( $columnName ) -> getType () -> getName ();
2017-03-26 23:43:19 +03:00
2017-07-19 17:17:46 +03:00
switch ( $type ) {
case Type :: BLOB :
case Type :: TEXT :
$this -> columnTypes [ $tableName ][ $columnName ] = IQueryBuilder :: PARAM_LOB ;
break ;
default :
$this -> columnTypes [ $tableName ][ $columnName ] = false ;
2017-03-26 18:11:57 +03:00
}
2017-07-19 17:17:46 +03:00
return $this -> columnTypes [ $tableName ][ $columnName ];
2017-03-26 18:11:57 +03:00
}
2014-04-09 17:21:57 +04:00
protected function convertDB ( Connection $fromDB , Connection $toDB , array $tables , InputInterface $input , OutputInterface $output ) {
2014-11-19 02:25:26 +03:00
$this -> config -> setSystemValue ( 'maintenance' , true );
2017-07-19 14:15:32 +03:00
$schema = $fromDB -> createSchema ();
2013-12-07 18:14:54 +04:00
try {
// copy table rows
foreach ( $tables as $table ) {
$output -> writeln ( $table );
2017-07-19 14:15:32 +03:00
$this -> copyTable ( $fromDB , $toDB , $schema -> getTable ( $table ), $input , $output );
2013-12-07 18:14:54 +04:00
}
2014-04-14 20:33:21 +04:00
if ( $input -> getArgument ( 'type' ) === 'pgsql' ) {
2014-12-22 12:44:30 +03:00
$tools = new \OC\DB\PgSqlTools ( $this -> config );
2014-04-14 20:33:21 +04:00
$tools -> resynchronizeDatabaseSequences ( $toDB );
2014-01-11 15:25:35 +04:00
}
2013-12-07 18:14:54 +04:00
// save new database config
2014-02-17 21:02:58 +04:00
$this -> saveDBInfo ( $input );
2014-02-12 20:42:55 +04:00
} catch ( \Exception $e ) {
2014-11-19 02:25:26 +03:00
$this -> config -> setSystemValue ( 'maintenance' , false );
2013-12-07 18:14:54 +04:00
throw $e ;
2013-12-07 17:44:23 +04:00
}
2014-11-19 02:25:26 +03:00
$this -> config -> setSystemValue ( 'maintenance' , false );
2013-12-07 17:44:23 +04:00
}
2014-04-09 17:21:57 +04:00
protected function saveDBInfo ( InputInterface $input ) {
2014-02-17 21:02:58 +04:00
$type = $input -> getArgument ( 'type' );
$username = $input -> getArgument ( 'username' );
2015-01-26 14:59:25 +03:00
$dbHost = $input -> getArgument ( 'hostname' );
$dbName = $input -> getArgument ( 'database' );
2014-02-17 21:02:58 +04:00
$password = $input -> getOption ( 'password' );
if ( $input -> getOption ( 'port' )) {
2015-01-26 14:59:25 +03:00
$dbHost .= ':' . $input -> getOption ( 'port' );
2013-12-07 17:44:23 +04:00
}
2014-02-17 21:02:58 +04:00
2015-01-23 13:13:47 +03:00
$this -> config -> setSystemValues ([
'dbtype' => $type ,
2015-01-26 14:59:25 +03:00
'dbname' => $dbName ,
'dbhost' => $dbHost ,
2015-01-23 13:13:47 +03:00
'dbuser' => $username ,
'dbpassword' => $password ,
]);
2013-12-07 17:44:23 +04:00
}
2016-09-21 15:21:39 +03:00
/**
* Return possible values for the named option
*
* @ param string $optionName
* @ param CompletionContext $context
* @ return string []
*/
public function completeOptionValues ( $optionName , CompletionContext $context ) {
return [];
}
/**
* Return possible values for the named argument
*
* @ param string $argumentName
* @ param CompletionContext $context
* @ return string []
*/
public function completeArgumentValues ( $argumentName , CompletionContext $context ) {
if ( $argumentName === 'type' ) {
return [ 'mysql' , 'oci' , 'pgsql' ];
}
return [];
}
2013-12-24 16:44:35 +04:00
}