Passed
Push — master ( 1c9781...932a38 )
by Christoph
17:10 queued 11s
created
core/Command/Db/ConvertType.php 1 patch
Indentation   +411 added lines, -411 removed lines patch added patch discarded remove patch
@@ -56,415 +56,415 @@
 block discarded – undo
56 56
 use function preg_quote;
57 57
 
58 58
 class ConvertType extends Command implements CompletionAwareInterface {
59
-	/**
60
-	 * @var \OCP\IConfig
61
-	 */
62
-	protected $config;
63
-
64
-	/**
65
-	 * @var \OC\DB\ConnectionFactory
66
-	 */
67
-	protected $connectionFactory;
68
-
69
-	/** @var array */
70
-	protected $columnTypes;
71
-
72
-	/**
73
-	 * @param \OCP\IConfig $config
74
-	 * @param \OC\DB\ConnectionFactory $connectionFactory
75
-	 */
76
-	public function __construct(IConfig $config, ConnectionFactory $connectionFactory) {
77
-		$this->config = $config;
78
-		$this->connectionFactory = $connectionFactory;
79
-		parent::__construct();
80
-	}
81
-
82
-	protected function configure() {
83
-		$this
84
-			->setName('db:convert-type')
85
-			->setDescription('Convert the Nextcloud database to the newly configured one')
86
-			->addArgument(
87
-				'type',
88
-				InputArgument::REQUIRED,
89
-				'the type of the database to convert to'
90
-			)
91
-			->addArgument(
92
-				'username',
93
-				InputArgument::REQUIRED,
94
-				'the username of the database to convert to'
95
-			)
96
-			->addArgument(
97
-				'hostname',
98
-				InputArgument::REQUIRED,
99
-				'the hostname of the database to convert to'
100
-			)
101
-			->addArgument(
102
-				'database',
103
-				InputArgument::REQUIRED,
104
-				'the name of the database to convert to'
105
-			)
106
-			->addOption(
107
-				'port',
108
-				null,
109
-				InputOption::VALUE_REQUIRED,
110
-				'the port of the database to convert to'
111
-			)
112
-			->addOption(
113
-				'password',
114
-				null,
115
-				InputOption::VALUE_REQUIRED,
116
-				'the password of the database to convert to. Will be asked when not specified. Can also be passed via stdin.'
117
-			)
118
-			->addOption(
119
-				'clear-schema',
120
-				null,
121
-				InputOption::VALUE_NONE,
122
-				'remove all tables from the destination database'
123
-			)
124
-			->addOption(
125
-				'all-apps',
126
-				null,
127
-				InputOption::VALUE_NONE,
128
-				'whether to create schema for all apps instead of only installed apps'
129
-			)
130
-			->addOption(
131
-				'chunk-size',
132
-				null,
133
-				InputOption::VALUE_REQUIRED,
134
-				'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.',
135
-				1000
136
-			)
137
-		;
138
-	}
139
-
140
-	protected function validateInput(InputInterface $input, OutputInterface $output) {
141
-		$type = $this->connectionFactory->normalizeType($input->getArgument('type'));
142
-		if ($type === 'sqlite3') {
143
-			throw new \InvalidArgumentException(
144
-				'Converting to SQLite (sqlite3) is currently not supported.'
145
-			);
146
-		}
147
-		if ($type === $this->config->getSystemValue('dbtype', '')) {
148
-			throw new \InvalidArgumentException(sprintf(
149
-				'Can not convert from %1$s to %1$s.',
150
-				$type
151
-			));
152
-		}
153
-		if ($type === 'oci' && $input->getOption('clear-schema')) {
154
-			// Doctrine unconditionally tries (at least in version 2.3)
155
-			// to drop sequence triggers when dropping a table, even though
156
-			// such triggers may not exist. This results in errors like
157
-			// "ORA-04080: trigger 'OC_STORAGES_AI_PK' does not exist".
158
-			throw new \InvalidArgumentException(
159
-				'The --clear-schema option is not supported when converting to Oracle (oci).'
160
-			);
161
-		}
162
-	}
163
-
164
-	protected function readPassword(InputInterface $input, OutputInterface $output) {
165
-		// Explicitly specified password
166
-		if ($input->getOption('password')) {
167
-			return;
168
-		}
169
-
170
-		// Read from stdin. stream_set_blocking is used to prevent blocking
171
-		// when nothing is passed via stdin.
172
-		stream_set_blocking(STDIN, 0);
173
-		$password = file_get_contents('php://stdin');
174
-		stream_set_blocking(STDIN, 1);
175
-		if (trim($password) !== '') {
176
-			$input->setOption('password', $password);
177
-			return;
178
-		}
179
-
180
-		// Read password by interacting
181
-		if ($input->isInteractive()) {
182
-			/** @var QuestionHelper $helper */
183
-			$helper = $this->getHelper('question');
184
-			$question = new Question('What is the database password?');
185
-			$question->setHidden(true);
186
-			$question->setHiddenFallback(false);
187
-			$password = $helper->ask($input, $output, $question);
188
-			$input->setOption('password', $password);
189
-			return;
190
-		}
191
-	}
192
-
193
-	protected function execute(InputInterface $input, OutputInterface $output): int {
194
-		$this->validateInput($input, $output);
195
-		$this->readPassword($input, $output);
196
-
197
-		/** @var Connection $fromDB */
198
-		$fromDB = \OC::$server->get(Connection::class);
199
-		$toDB = $this->getToDBConnection($input, $output);
200
-
201
-		if ($input->getOption('clear-schema')) {
202
-			$this->clearSchema($toDB, $input, $output);
203
-		}
204
-
205
-		$this->createSchema($fromDB, $toDB, $input, $output);
206
-
207
-		$toTables = $this->getTables($toDB);
208
-		$fromTables = $this->getTables($fromDB);
209
-
210
-		// warn/fail if there are more tables in 'from' database
211
-		$extraFromTables = array_diff($fromTables, $toTables);
212
-		if (!empty($extraFromTables)) {
213
-			$output->writeln('<comment>The following tables will not be converted:</comment>');
214
-			$output->writeln($extraFromTables);
215
-			if (!$input->getOption('all-apps')) {
216
-				$output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>');
217
-				$output->writeln('<comment>can be included by specifying the --all-apps option.</comment>');
218
-			}
219
-
220
-			$continueConversion = !$input->isInteractive(); // assume yes for --no-interaction and no otherwise.
221
-			$question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', $continueConversion);
222
-
223
-			/** @var QuestionHelper $helper */
224
-			$helper = $this->getHelper('question');
225
-
226
-			if (!$helper->ask($input, $output, $question)) {
227
-				return 1;
228
-			}
229
-		}
230
-		$intersectingTables = array_intersect($toTables, $fromTables);
231
-		$this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output);
232
-		return 0;
233
-	}
234
-
235
-	protected function createSchema(Connection $fromDB, Connection $toDB, InputInterface $input, OutputInterface $output) {
236
-		$output->writeln('<info>Creating schema in new database</info>');
237
-
238
-		$fromMS = new MigrationService('core', $fromDB);
239
-		$currentMigration = $fromMS->getMigration('current');
240
-		if ($currentMigration !== '0') {
241
-			$toMS = new MigrationService('core', $toDB);
242
-			$toMS->migrate($currentMigration);
243
-		}
244
-
245
-		$apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps();
246
-		foreach ($apps as $app) {
247
-			$output->writeln('<info> - '.$app.'</info>');
248
-			// Make sure autoloading works...
249
-			\OC_App::loadApp($app);
250
-			$fromMS = new MigrationService($app, $fromDB);
251
-			$currentMigration = $fromMS->getMigration('current');
252
-			if ($currentMigration !== '0') {
253
-				$toMS = new MigrationService($app, $toDB);
254
-				$toMS->migrate($currentMigration, true);
255
-			}
256
-		}
257
-	}
258
-
259
-	protected function getToDBConnection(InputInterface $input, OutputInterface $output) {
260
-		$type = $input->getArgument('type');
261
-		$connectionParams = $this->connectionFactory->createConnectionParams();
262
-		$connectionParams = array_merge($connectionParams, [
263
-			'host' => $input->getArgument('hostname'),
264
-			'user' => $input->getArgument('username'),
265
-			'password' => $input->getOption('password'),
266
-			'dbname' => $input->getArgument('database'),
267
-		]);
268
-		if ($input->getOption('port')) {
269
-			$connectionParams['port'] = $input->getOption('port');
270
-		}
271
-		return $this->connectionFactory->getConnection($type, $connectionParams);
272
-	}
273
-
274
-	protected function clearSchema(Connection $db, InputInterface $input, OutputInterface $output) {
275
-		$toTables = $this->getTables($db);
276
-		if (!empty($toTables)) {
277
-			$output->writeln('<info>Clearing schema in new database</info>');
278
-		}
279
-		foreach ($toTables as $table) {
280
-			$db->getSchemaManager()->dropTable($table);
281
-		}
282
-	}
283
-
284
-	protected function getTables(Connection $db) {
285
-		$db->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
286
-			/** @var string|AbstractAsset $asset */
287
-			$filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
288
-			if ($asset instanceof AbstractAsset) {
289
-				return preg_match($filterExpression, $asset->getName()) !== false;
290
-			}
291
-			return preg_match($filterExpression, $asset) !== false;
292
-		});
293
-		return $db->getSchemaManager()->listTableNames();
294
-	}
295
-
296
-	/**
297
-	 * @param Connection $fromDB
298
-	 * @param Connection $toDB
299
-	 * @param Table $table
300
-	 * @param InputInterface $input
301
-	 * @param OutputInterface $output
302
-	 */
303
-	protected function copyTable(Connection $fromDB, Connection $toDB, Table $table, InputInterface $input, OutputInterface $output) {
304
-		if ($table->getName() === $toDB->getPrefix() . 'migrations') {
305
-			$output->writeln('<comment>Skipping migrations table because it was already filled by running the migrations</comment>');
306
-			return;
307
-		}
308
-
309
-		$chunkSize = $input->getOption('chunk-size');
310
-
311
-		$query = $fromDB->getQueryBuilder();
312
-		$query->automaticTablePrefix(false);
313
-		$query->select($query->func()->count('*', 'num_entries'))
314
-			->from($table->getName());
315
-		$result = $query->execute();
316
-		$count = $result->fetchOne();
317
-		$result->closeCursor();
318
-
319
-		$numChunks = ceil($count / $chunkSize);
320
-		if ($numChunks > 1) {
321
-			$output->writeln('chunked query, ' . $numChunks . ' chunks');
322
-		}
323
-
324
-		$progress = new ProgressBar($output, $count);
325
-		$progress->setFormat('very_verbose');
326
-		$progress->start();
327
-		$redraw = $count > $chunkSize ? 100 : ($count > 100 ? 5 : 1);
328
-		$progress->setRedrawFrequency($redraw);
329
-
330
-		$query = $fromDB->getQueryBuilder();
331
-		$query->automaticTablePrefix(false);
332
-		$query->select('*')
333
-			->from($table->getName())
334
-			->setMaxResults($chunkSize);
335
-
336
-		try {
337
-			$orderColumns = $table->getPrimaryKeyColumns();
338
-		} catch (Exception $e) {
339
-			$orderColumns = $table->getColumns();
340
-		}
341
-
342
-		foreach ($orderColumns as $column) {
343
-			$query->addOrderBy($column->getName());
344
-		}
345
-
346
-		$insertQuery = $toDB->getQueryBuilder();
347
-		$insertQuery->automaticTablePrefix(false);
348
-		$insertQuery->insert($table->getName());
349
-		$parametersCreated = false;
350
-
351
-		for ($chunk = 0; $chunk < $numChunks; $chunk++) {
352
-			$query->setFirstResult($chunk * $chunkSize);
353
-
354
-			$result = $query->execute();
355
-
356
-			while ($row = $result->fetch()) {
357
-				$progress->advance();
358
-				if (!$parametersCreated) {
359
-					foreach ($row as $key => $value) {
360
-						$insertQuery->setValue($key, $insertQuery->createParameter($key));
361
-					}
362
-					$parametersCreated = true;
363
-				}
364
-
365
-				foreach ($row as $key => $value) {
366
-					$type = $this->getColumnType($table, $key);
367
-					if ($type !== false) {
368
-						$insertQuery->setParameter($key, $value, $type);
369
-					} else {
370
-						$insertQuery->setParameter($key, $value);
371
-					}
372
-				}
373
-				$insertQuery->execute();
374
-			}
375
-			$result->closeCursor();
376
-		}
377
-		$progress->finish();
378
-		$output->writeln('');
379
-	}
380
-
381
-	protected function getColumnType(Table $table, $columnName) {
382
-		$tableName = $table->getName();
383
-		if (isset($this->columnTypes[$tableName][$columnName])) {
384
-			return $this->columnTypes[$tableName][$columnName];
385
-		}
386
-
387
-		$type = $table->getColumn($columnName)->getType()->getName();
388
-
389
-		switch ($type) {
390
-			case Types::BLOB:
391
-			case Types::TEXT:
392
-				$this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_LOB;
393
-				break;
394
-			case Types::BOOLEAN:
395
-				$this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_BOOL;
396
-				break;
397
-			default:
398
-				$this->columnTypes[$tableName][$columnName] = false;
399
-		}
400
-
401
-		return $this->columnTypes[$tableName][$columnName];
402
-	}
403
-
404
-	protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) {
405
-		$this->config->setSystemValue('maintenance', true);
406
-		$schema = $fromDB->createSchema();
407
-
408
-		try {
409
-			// copy table rows
410
-			foreach ($tables as $table) {
411
-				$output->writeln('<info> - '.$table.'</info>');
412
-				$this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output);
413
-			}
414
-			if ($input->getArgument('type') === 'pgsql') {
415
-				$tools = new \OC\DB\PgSqlTools($this->config);
416
-				$tools->resynchronizeDatabaseSequences($toDB);
417
-			}
418
-			// save new database config
419
-			$this->saveDBInfo($input);
420
-		} catch (\Exception $e) {
421
-			$this->config->setSystemValue('maintenance', false);
422
-			throw $e;
423
-		}
424
-		$this->config->setSystemValue('maintenance', false);
425
-	}
426
-
427
-	protected function saveDBInfo(InputInterface $input) {
428
-		$type = $input->getArgument('type');
429
-		$username = $input->getArgument('username');
430
-		$dbHost = $input->getArgument('hostname');
431
-		$dbName = $input->getArgument('database');
432
-		$password = $input->getOption('password');
433
-		if ($input->getOption('port')) {
434
-			$dbHost .= ':'.$input->getOption('port');
435
-		}
436
-
437
-		$this->config->setSystemValues([
438
-			'dbtype' => $type,
439
-			'dbname' => $dbName,
440
-			'dbhost' => $dbHost,
441
-			'dbuser' => $username,
442
-			'dbpassword' => $password,
443
-		]);
444
-	}
445
-
446
-	/**
447
-	 * Return possible values for the named option
448
-	 *
449
-	 * @param string $optionName
450
-	 * @param CompletionContext $context
451
-	 * @return string[]
452
-	 */
453
-	public function completeOptionValues($optionName, CompletionContext $context) {
454
-		return [];
455
-	}
456
-
457
-	/**
458
-	 * Return possible values for the named argument
459
-	 *
460
-	 * @param string $argumentName
461
-	 * @param CompletionContext $context
462
-	 * @return string[]
463
-	 */
464
-	public function completeArgumentValues($argumentName, CompletionContext $context) {
465
-		if ($argumentName === 'type') {
466
-			return ['mysql', 'oci', 'pgsql'];
467
-		}
468
-		return [];
469
-	}
59
+    /**
60
+     * @var \OCP\IConfig
61
+     */
62
+    protected $config;
63
+
64
+    /**
65
+     * @var \OC\DB\ConnectionFactory
66
+     */
67
+    protected $connectionFactory;
68
+
69
+    /** @var array */
70
+    protected $columnTypes;
71
+
72
+    /**
73
+     * @param \OCP\IConfig $config
74
+     * @param \OC\DB\ConnectionFactory $connectionFactory
75
+     */
76
+    public function __construct(IConfig $config, ConnectionFactory $connectionFactory) {
77
+        $this->config = $config;
78
+        $this->connectionFactory = $connectionFactory;
79
+        parent::__construct();
80
+    }
81
+
82
+    protected function configure() {
83
+        $this
84
+            ->setName('db:convert-type')
85
+            ->setDescription('Convert the Nextcloud database to the newly configured one')
86
+            ->addArgument(
87
+                'type',
88
+                InputArgument::REQUIRED,
89
+                'the type of the database to convert to'
90
+            )
91
+            ->addArgument(
92
+                'username',
93
+                InputArgument::REQUIRED,
94
+                'the username of the database to convert to'
95
+            )
96
+            ->addArgument(
97
+                'hostname',
98
+                InputArgument::REQUIRED,
99
+                'the hostname of the database to convert to'
100
+            )
101
+            ->addArgument(
102
+                'database',
103
+                InputArgument::REQUIRED,
104
+                'the name of the database to convert to'
105
+            )
106
+            ->addOption(
107
+                'port',
108
+                null,
109
+                InputOption::VALUE_REQUIRED,
110
+                'the port of the database to convert to'
111
+            )
112
+            ->addOption(
113
+                'password',
114
+                null,
115
+                InputOption::VALUE_REQUIRED,
116
+                'the password of the database to convert to. Will be asked when not specified. Can also be passed via stdin.'
117
+            )
118
+            ->addOption(
119
+                'clear-schema',
120
+                null,
121
+                InputOption::VALUE_NONE,
122
+                'remove all tables from the destination database'
123
+            )
124
+            ->addOption(
125
+                'all-apps',
126
+                null,
127
+                InputOption::VALUE_NONE,
128
+                'whether to create schema for all apps instead of only installed apps'
129
+            )
130
+            ->addOption(
131
+                'chunk-size',
132
+                null,
133
+                InputOption::VALUE_REQUIRED,
134
+                '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.',
135
+                1000
136
+            )
137
+        ;
138
+    }
139
+
140
+    protected function validateInput(InputInterface $input, OutputInterface $output) {
141
+        $type = $this->connectionFactory->normalizeType($input->getArgument('type'));
142
+        if ($type === 'sqlite3') {
143
+            throw new \InvalidArgumentException(
144
+                'Converting to SQLite (sqlite3) is currently not supported.'
145
+            );
146
+        }
147
+        if ($type === $this->config->getSystemValue('dbtype', '')) {
148
+            throw new \InvalidArgumentException(sprintf(
149
+                'Can not convert from %1$s to %1$s.',
150
+                $type
151
+            ));
152
+        }
153
+        if ($type === 'oci' && $input->getOption('clear-schema')) {
154
+            // Doctrine unconditionally tries (at least in version 2.3)
155
+            // to drop sequence triggers when dropping a table, even though
156
+            // such triggers may not exist. This results in errors like
157
+            // "ORA-04080: trigger 'OC_STORAGES_AI_PK' does not exist".
158
+            throw new \InvalidArgumentException(
159
+                'The --clear-schema option is not supported when converting to Oracle (oci).'
160
+            );
161
+        }
162
+    }
163
+
164
+    protected function readPassword(InputInterface $input, OutputInterface $output) {
165
+        // Explicitly specified password
166
+        if ($input->getOption('password')) {
167
+            return;
168
+        }
169
+
170
+        // Read from stdin. stream_set_blocking is used to prevent blocking
171
+        // when nothing is passed via stdin.
172
+        stream_set_blocking(STDIN, 0);
173
+        $password = file_get_contents('php://stdin');
174
+        stream_set_blocking(STDIN, 1);
175
+        if (trim($password) !== '') {
176
+            $input->setOption('password', $password);
177
+            return;
178
+        }
179
+
180
+        // Read password by interacting
181
+        if ($input->isInteractive()) {
182
+            /** @var QuestionHelper $helper */
183
+            $helper = $this->getHelper('question');
184
+            $question = new Question('What is the database password?');
185
+            $question->setHidden(true);
186
+            $question->setHiddenFallback(false);
187
+            $password = $helper->ask($input, $output, $question);
188
+            $input->setOption('password', $password);
189
+            return;
190
+        }
191
+    }
192
+
193
+    protected function execute(InputInterface $input, OutputInterface $output): int {
194
+        $this->validateInput($input, $output);
195
+        $this->readPassword($input, $output);
196
+
197
+        /** @var Connection $fromDB */
198
+        $fromDB = \OC::$server->get(Connection::class);
199
+        $toDB = $this->getToDBConnection($input, $output);
200
+
201
+        if ($input->getOption('clear-schema')) {
202
+            $this->clearSchema($toDB, $input, $output);
203
+        }
204
+
205
+        $this->createSchema($fromDB, $toDB, $input, $output);
206
+
207
+        $toTables = $this->getTables($toDB);
208
+        $fromTables = $this->getTables($fromDB);
209
+
210
+        // warn/fail if there are more tables in 'from' database
211
+        $extraFromTables = array_diff($fromTables, $toTables);
212
+        if (!empty($extraFromTables)) {
213
+            $output->writeln('<comment>The following tables will not be converted:</comment>');
214
+            $output->writeln($extraFromTables);
215
+            if (!$input->getOption('all-apps')) {
216
+                $output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>');
217
+                $output->writeln('<comment>can be included by specifying the --all-apps option.</comment>');
218
+            }
219
+
220
+            $continueConversion = !$input->isInteractive(); // assume yes for --no-interaction and no otherwise.
221
+            $question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', $continueConversion);
222
+
223
+            /** @var QuestionHelper $helper */
224
+            $helper = $this->getHelper('question');
225
+
226
+            if (!$helper->ask($input, $output, $question)) {
227
+                return 1;
228
+            }
229
+        }
230
+        $intersectingTables = array_intersect($toTables, $fromTables);
231
+        $this->convertDB($fromDB, $toDB, $intersectingTables, $input, $output);
232
+        return 0;
233
+    }
234
+
235
+    protected function createSchema(Connection $fromDB, Connection $toDB, InputInterface $input, OutputInterface $output) {
236
+        $output->writeln('<info>Creating schema in new database</info>');
237
+
238
+        $fromMS = new MigrationService('core', $fromDB);
239
+        $currentMigration = $fromMS->getMigration('current');
240
+        if ($currentMigration !== '0') {
241
+            $toMS = new MigrationService('core', $toDB);
242
+            $toMS->migrate($currentMigration);
243
+        }
244
+
245
+        $apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps();
246
+        foreach ($apps as $app) {
247
+            $output->writeln('<info> - '.$app.'</info>');
248
+            // Make sure autoloading works...
249
+            \OC_App::loadApp($app);
250
+            $fromMS = new MigrationService($app, $fromDB);
251
+            $currentMigration = $fromMS->getMigration('current');
252
+            if ($currentMigration !== '0') {
253
+                $toMS = new MigrationService($app, $toDB);
254
+                $toMS->migrate($currentMigration, true);
255
+            }
256
+        }
257
+    }
258
+
259
+    protected function getToDBConnection(InputInterface $input, OutputInterface $output) {
260
+        $type = $input->getArgument('type');
261
+        $connectionParams = $this->connectionFactory->createConnectionParams();
262
+        $connectionParams = array_merge($connectionParams, [
263
+            'host' => $input->getArgument('hostname'),
264
+            'user' => $input->getArgument('username'),
265
+            'password' => $input->getOption('password'),
266
+            'dbname' => $input->getArgument('database'),
267
+        ]);
268
+        if ($input->getOption('port')) {
269
+            $connectionParams['port'] = $input->getOption('port');
270
+        }
271
+        return $this->connectionFactory->getConnection($type, $connectionParams);
272
+    }
273
+
274
+    protected function clearSchema(Connection $db, InputInterface $input, OutputInterface $output) {
275
+        $toTables = $this->getTables($db);
276
+        if (!empty($toTables)) {
277
+            $output->writeln('<info>Clearing schema in new database</info>');
278
+        }
279
+        foreach ($toTables as $table) {
280
+            $db->getSchemaManager()->dropTable($table);
281
+        }
282
+    }
283
+
284
+    protected function getTables(Connection $db) {
285
+        $db->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
286
+            /** @var string|AbstractAsset $asset */
287
+            $filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/';
288
+            if ($asset instanceof AbstractAsset) {
289
+                return preg_match($filterExpression, $asset->getName()) !== false;
290
+            }
291
+            return preg_match($filterExpression, $asset) !== false;
292
+        });
293
+        return $db->getSchemaManager()->listTableNames();
294
+    }
295
+
296
+    /**
297
+     * @param Connection $fromDB
298
+     * @param Connection $toDB
299
+     * @param Table $table
300
+     * @param InputInterface $input
301
+     * @param OutputInterface $output
302
+     */
303
+    protected function copyTable(Connection $fromDB, Connection $toDB, Table $table, InputInterface $input, OutputInterface $output) {
304
+        if ($table->getName() === $toDB->getPrefix() . 'migrations') {
305
+            $output->writeln('<comment>Skipping migrations table because it was already filled by running the migrations</comment>');
306
+            return;
307
+        }
308
+
309
+        $chunkSize = $input->getOption('chunk-size');
310
+
311
+        $query = $fromDB->getQueryBuilder();
312
+        $query->automaticTablePrefix(false);
313
+        $query->select($query->func()->count('*', 'num_entries'))
314
+            ->from($table->getName());
315
+        $result = $query->execute();
316
+        $count = $result->fetchOne();
317
+        $result->closeCursor();
318
+
319
+        $numChunks = ceil($count / $chunkSize);
320
+        if ($numChunks > 1) {
321
+            $output->writeln('chunked query, ' . $numChunks . ' chunks');
322
+        }
323
+
324
+        $progress = new ProgressBar($output, $count);
325
+        $progress->setFormat('very_verbose');
326
+        $progress->start();
327
+        $redraw = $count > $chunkSize ? 100 : ($count > 100 ? 5 : 1);
328
+        $progress->setRedrawFrequency($redraw);
329
+
330
+        $query = $fromDB->getQueryBuilder();
331
+        $query->automaticTablePrefix(false);
332
+        $query->select('*')
333
+            ->from($table->getName())
334
+            ->setMaxResults($chunkSize);
335
+
336
+        try {
337
+            $orderColumns = $table->getPrimaryKeyColumns();
338
+        } catch (Exception $e) {
339
+            $orderColumns = $table->getColumns();
340
+        }
341
+
342
+        foreach ($orderColumns as $column) {
343
+            $query->addOrderBy($column->getName());
344
+        }
345
+
346
+        $insertQuery = $toDB->getQueryBuilder();
347
+        $insertQuery->automaticTablePrefix(false);
348
+        $insertQuery->insert($table->getName());
349
+        $parametersCreated = false;
350
+
351
+        for ($chunk = 0; $chunk < $numChunks; $chunk++) {
352
+            $query->setFirstResult($chunk * $chunkSize);
353
+
354
+            $result = $query->execute();
355
+
356
+            while ($row = $result->fetch()) {
357
+                $progress->advance();
358
+                if (!$parametersCreated) {
359
+                    foreach ($row as $key => $value) {
360
+                        $insertQuery->setValue($key, $insertQuery->createParameter($key));
361
+                    }
362
+                    $parametersCreated = true;
363
+                }
364
+
365
+                foreach ($row as $key => $value) {
366
+                    $type = $this->getColumnType($table, $key);
367
+                    if ($type !== false) {
368
+                        $insertQuery->setParameter($key, $value, $type);
369
+                    } else {
370
+                        $insertQuery->setParameter($key, $value);
371
+                    }
372
+                }
373
+                $insertQuery->execute();
374
+            }
375
+            $result->closeCursor();
376
+        }
377
+        $progress->finish();
378
+        $output->writeln('');
379
+    }
380
+
381
+    protected function getColumnType(Table $table, $columnName) {
382
+        $tableName = $table->getName();
383
+        if (isset($this->columnTypes[$tableName][$columnName])) {
384
+            return $this->columnTypes[$tableName][$columnName];
385
+        }
386
+
387
+        $type = $table->getColumn($columnName)->getType()->getName();
388
+
389
+        switch ($type) {
390
+            case Types::BLOB:
391
+            case Types::TEXT:
392
+                $this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_LOB;
393
+                break;
394
+            case Types::BOOLEAN:
395
+                $this->columnTypes[$tableName][$columnName] = IQueryBuilder::PARAM_BOOL;
396
+                break;
397
+            default:
398
+                $this->columnTypes[$tableName][$columnName] = false;
399
+        }
400
+
401
+        return $this->columnTypes[$tableName][$columnName];
402
+    }
403
+
404
+    protected function convertDB(Connection $fromDB, Connection $toDB, array $tables, InputInterface $input, OutputInterface $output) {
405
+        $this->config->setSystemValue('maintenance', true);
406
+        $schema = $fromDB->createSchema();
407
+
408
+        try {
409
+            // copy table rows
410
+            foreach ($tables as $table) {
411
+                $output->writeln('<info> - '.$table.'</info>');
412
+                $this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output);
413
+            }
414
+            if ($input->getArgument('type') === 'pgsql') {
415
+                $tools = new \OC\DB\PgSqlTools($this->config);
416
+                $tools->resynchronizeDatabaseSequences($toDB);
417
+            }
418
+            // save new database config
419
+            $this->saveDBInfo($input);
420
+        } catch (\Exception $e) {
421
+            $this->config->setSystemValue('maintenance', false);
422
+            throw $e;
423
+        }
424
+        $this->config->setSystemValue('maintenance', false);
425
+    }
426
+
427
+    protected function saveDBInfo(InputInterface $input) {
428
+        $type = $input->getArgument('type');
429
+        $username = $input->getArgument('username');
430
+        $dbHost = $input->getArgument('hostname');
431
+        $dbName = $input->getArgument('database');
432
+        $password = $input->getOption('password');
433
+        if ($input->getOption('port')) {
434
+            $dbHost .= ':'.$input->getOption('port');
435
+        }
436
+
437
+        $this->config->setSystemValues([
438
+            'dbtype' => $type,
439
+            'dbname' => $dbName,
440
+            'dbhost' => $dbHost,
441
+            'dbuser' => $username,
442
+            'dbpassword' => $password,
443
+        ]);
444
+    }
445
+
446
+    /**
447
+     * Return possible values for the named option
448
+     *
449
+     * @param string $optionName
450
+     * @param CompletionContext $context
451
+     * @return string[]
452
+     */
453
+    public function completeOptionValues($optionName, CompletionContext $context) {
454
+        return [];
455
+    }
456
+
457
+    /**
458
+     * Return possible values for the named argument
459
+     *
460
+     * @param string $argumentName
461
+     * @param CompletionContext $context
462
+     * @return string[]
463
+     */
464
+    public function completeArgumentValues($argumentName, CompletionContext $context) {
465
+        if ($argumentName === 'type') {
466
+            return ['mysql', 'oci', 'pgsql'];
467
+        }
468
+        return [];
469
+    }
470 470
 }
Please login to merge, or discard this patch.