diff options
Diffstat (limited to 'core/Command/Db')
-rw-r--r-- | core/Command/Db/AddMissingColumns.php | 24 | ||||
-rw-r--r-- | core/Command/Db/AddMissingIndices.php | 46 | ||||
-rw-r--r-- | core/Command/Db/AddMissingPrimaryKeys.php | 24 | ||||
-rw-r--r-- | core/Command/Db/ConvertFilecacheBigInt.php | 37 | ||||
-rw-r--r-- | core/Command/Db/ConvertMysqlToMB4.php | 31 | ||||
-rw-r--r-- | core/Command/Db/ConvertType.php | 83 | ||||
-rw-r--r-- | core/Command/Db/ExpectedSchema.php | 68 | ||||
-rw-r--r-- | core/Command/Db/ExportSchema.php | 44 | ||||
-rw-r--r-- | core/Command/Db/Migrations/ExecuteCommand.php | 24 | ||||
-rw-r--r-- | core/Command/Db/Migrations/GenerateCommand.php | 71 | ||||
-rw-r--r-- | core/Command/Db/Migrations/GenerateMetadataCommand.php | 79 | ||||
-rw-r--r-- | core/Command/Db/Migrations/MigrateCommand.php | 23 | ||||
-rw-r--r-- | core/Command/Db/Migrations/PreviewCommand.php | 111 | ||||
-rw-r--r-- | core/Command/Db/Migrations/StatusCommand.php | 24 | ||||
-rw-r--r-- | core/Command/Db/SchemaEncoder.php | 115 |
15 files changed, 517 insertions, 287 deletions
diff --git a/core/Command/Db/AddMissingColumns.php b/core/Command/Db/AddMissingColumns.php index 07763c66154..33b4b24a6cb 100644 --- a/core/Command/Db/AddMissingColumns.php +++ b/core/Command/Db/AddMissingColumns.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -55,7 +37,7 @@ class AddMissingColumns extends Command { $this ->setName('db:add-missing-columns') ->setDescription('Add missing optional columns to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/core/Command/Db/AddMissingIndices.php b/core/Command/Db/AddMissingIndices.php index 1e10b6152ce..eec0aedce11 100644 --- a/core/Command/Db/AddMissingIndices.php +++ b/core/Command/Db/AddMissingIndices.php @@ -3,33 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Mario Danic <mario@lovelyhq.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -62,7 +37,7 @@ class AddMissingIndices extends Command { $this ->setName('db:add-missing-indices') ->setDescription('Add missing indices to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -111,14 +86,7 @@ class AddMissingIndices extends Command { if ($schema->hasTable($toReplaceIndex['tableName'])) { $table = $schema->getTable($toReplaceIndex['tableName']); - $allOldIndicesExists = true; - foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { - if (!$table->hasIndex($oldIndexName)) { - $allOldIndicesExists = false; - } - } - - if (!$allOldIndicesExists) { + if ($table->hasIndex($toReplaceIndex['newIndexName'])) { continue; } @@ -135,8 +103,10 @@ class AddMissingIndices extends Command { } foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { - $output->writeln('<info>Removing ' . $oldIndexName . ' index from the ' . $table->getName() . ' table</info>'); - $table->dropIndex($oldIndexName); + if ($table->hasIndex($oldIndexName)) { + $output->writeln('<info>Removing ' . $oldIndexName . ' index from the ' . $table->getName() . ' table</info>'); + $table->dropIndex($oldIndexName); + } } if (!$dryRun) { diff --git a/core/Command/Db/AddMissingPrimaryKeys.php b/core/Command/Db/AddMissingPrimaryKeys.php index 658eb0b0f5a..1eb11c894fa 100644 --- a/core/Command/Db/AddMissingPrimaryKeys.php +++ b/core/Command/Db/AddMissingPrimaryKeys.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -55,7 +37,7 @@ class AddMissingPrimaryKeys extends Command { $this ->setName('db:add-missing-primary-keys') ->setDescription('Add missing primary keys to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/core/Command/Db/ConvertFilecacheBigInt.php b/core/Command/Db/ConvertFilecacheBigInt.php index 44cd81cd7eb..0d96d139701 100644 --- a/core/Command/Db/ConvertFilecacheBigInt.php +++ b/core/Command/Db/ConvertFilecacheBigInt.php @@ -1,42 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * - * @author Alecks Gates <alecks.g@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author timm2k <timm2k@gmx.de> - * @author Vincent Petry <vincent@nextcloud.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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; -use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Types\Type; use OC\DB\Connection; use OC\DB\SchemaWrapper; use OCP\DB\Types; +use OCP\IDBConnection; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; @@ -79,7 +55,7 @@ class ConvertFilecacheBigInt extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $schema = new SchemaWrapper($this->connection); - $isSqlite = $this->connection->getDatabasePlatform() instanceof SqlitePlatform; + $isSqlite = $this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE; $updates = []; $tables = static::getColumnsByTable(); @@ -115,6 +91,7 @@ class ConvertFilecacheBigInt extends Command { $output->writeln('<comment>This can take up to hours, depending on the number of files in your instance!</comment>'); if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', false); diff --git a/core/Command/Db/ConvertMysqlToMB4.php b/core/Command/Db/ConvertMysqlToMB4.php index a66fb46fc51..926e56c4300 100644 --- a/core/Command/Db/ConvertMysqlToMB4.php +++ b/core/Command/Db/ConvertMysqlToMB4.php @@ -1,30 +1,11 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.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/> - * + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db; -use Doctrine\DBAL\Platforms\MySQLPlatform; use OC\DB\MySqlTools; use OC\Migration\ConsoleOutput; use OC\Repair\Collation; @@ -53,15 +34,15 @@ class ConvertMysqlToMB4 extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) { - $output->writeln("This command is only valid for MySQL/MariaDB databases."); + if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_MYSQL) { + $output->writeln('This command is only valid for MySQL/MariaDB databases.'); return 1; } $tools = new MySqlTools(); if (!$tools->supports4ByteCharset($this->connection)) { $url = $this->urlGenerator->linkToDocs('admin-mysql-utf8mb4'); - $output->writeln("The database is not properly setup to use the charset utf8mb4."); + $output->writeln('The database is not properly setup to use the charset utf8mb4.'); $output->writeln("For more information please read the documentation at $url"); return 1; } diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index db618e938c0..0067bec4d9e 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -1,34 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Ostertag <bernieo.code@gmx.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Łukasz Buśko <busko.lukasz@pm.me> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sander Ruitenbeek <sander@grids.be> - * @author Simon Spannagel <simonspa@kth.se> - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db; @@ -38,9 +13,12 @@ use Doctrine\DBAL\Schema\Table; use OC\DB\Connection; use OC\DB\ConnectionFactory; use OC\DB\MigrationService; +use OC\DB\PgSqlTools; +use OCP\App\IAppManager; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Types; use OCP\IConfig; +use OCP\Server; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; @@ -61,6 +39,7 @@ class ConvertType extends Command implements CompletionAwareInterface { public function __construct( protected IConfig $config, protected ConnectionFactory $connectionFactory, + protected IAppManager $appManager, ) { parent::__construct(); } @@ -167,10 +146,13 @@ class ConvertType extends Command implements CompletionAwareInterface { if ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question('What is the database password?'); + $question = new Question('What is the database password (press <enter> for none)? '); $question->setHidden(true); $question->setHiddenFallback(false); $password = $helper->ask($input, $output, $question); + if ($password === null) { + $password = ''; // possibly unnecessary + } $input->setOption('password', $password); return; } @@ -181,7 +163,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $this->readPassword($input, $output); /** @var Connection $fromDB */ - $fromDB = \OC::$server->get(Connection::class); + $fromDB = Server::get(Connection::class); $toDB = $this->getToDBConnection($input, $output); if ($input->getOption('clear-schema')) { @@ -228,11 +210,13 @@ class ConvertType extends Command implements CompletionAwareInterface { $toMS->migrate($currentMigration); } - $apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps(); + $apps = $input->getOption('all-apps') + ? $this->appManager->getAllAppsInAppsFolders() + : $this->appManager->getEnabledApps(); foreach ($apps as $app) { - $output->writeln('<info> - '.$app.'</info>'); + $output->writeln('<info> - ' . $app . '</info>'); // Make sure autoloading works... - \OC_App::loadApp($app); + $this->appManager->loadApp($app); $fromMS = new MigrationService($app, $fromDB); $currentMigration = $fromMS->getMigration('current'); if ($currentMigration !== '0') { @@ -244,16 +228,31 @@ class ConvertType extends Command implements CompletionAwareInterface { protected function getToDBConnection(InputInterface $input, OutputInterface $output) { $type = $input->getArgument('type'); - $connectionParams = $this->connectionFactory->createConnectionParams(); + $connectionParams = $this->connectionFactory->createConnectionParams(type: $type); $connectionParams = array_merge($connectionParams, [ 'host' => $input->getArgument('hostname'), 'user' => $input->getArgument('username'), 'password' => $input->getOption('password'), 'dbname' => $input->getArgument('database'), ]); + + // parse port if ($input->getOption('port')) { $connectionParams['port'] = $input->getOption('port'); } + + // parse hostname for unix socket + if (preg_match('/^(.+)(:(\d+|[^:]+))?$/', $input->getArgument('hostname'), $matches)) { + $connectionParams['host'] = $matches[1]; + if (isset($matches[3])) { + if (is_numeric($matches[3])) { + $connectionParams['port'] = $matches[3]; + } else { + $connectionParams['unix_socket'] = $matches[3]; + } + } + } + return $this->connectionFactory->getConnection($type, $connectionParams); } @@ -263,7 +262,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $output->writeln('<info>Clearing schema in new database</info>'); } foreach ($toTables as $table) { - $db->getSchemaManager()->dropTable($table); + $db->createSchemaManager()->dropTable($table); } } @@ -276,7 +275,7 @@ class ConvertType extends Command implements CompletionAwareInterface { } return preg_match($filterExpression, $asset) !== false; }); - return $db->getSchemaManager()->listTableNames(); + return $db->createSchemaManager()->listTableNames(); } /** @@ -298,7 +297,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $query->automaticTablePrefix(false); $query->select($query->func()->count('*', 'num_entries')) ->from($table->getName()); - $result = $query->execute(); + $result = $query->executeQuery(); $count = $result->fetchOne(); $result->closeCursor(); @@ -337,7 +336,7 @@ class ConvertType extends Command implements CompletionAwareInterface { for ($chunk = 0; $chunk < $numChunks; $chunk++) { $query->setFirstResult($chunk * $chunkSize); - $result = $query->execute(); + $result = $query->executeQuery(); try { $toDB->beginTransaction(); @@ -404,11 +403,11 @@ class ConvertType extends Command implements CompletionAwareInterface { try { // copy table rows foreach ($tables as $table) { - $output->writeln('<info> - '.$table.'</info>'); + $output->writeln('<info> - ' . $table . '</info>'); $this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output); } if ($input->getArgument('type') === 'pgsql') { - $tools = new \OC\DB\PgSqlTools($this->config); + $tools = new PgSqlTools($this->config); $tools->resynchronizeDatabaseSequences($toDB); } // save new database config @@ -427,7 +426,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $dbName = $input->getArgument('database'); $password = $input->getOption('password'); if ($input->getOption('port')) { - $dbHost .= ':'.$input->getOption('port'); + $dbHost .= ':' . $input->getOption('port'); } $this->config->setSystemValues([ diff --git a/core/Command/Db/ExpectedSchema.php b/core/Command/Db/ExpectedSchema.php new file mode 100644 index 00000000000..1f35daba089 --- /dev/null +++ b/core/Command/Db/ExpectedSchema.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use Doctrine\DBAL\Schema\Schema; +use OC\Core\Command\Base; +use OC\DB\Connection; +use OC\DB\MigrationService; +use OC\DB\SchemaWrapper; +use OC\Migration\NullOutput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ExpectedSchema extends Base { + public function __construct( + protected Connection $connection, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('db:schema:expected') + ->setDescription('Export the expected database schema for a fresh installation') + ->setHelp("Note that the expected schema might not exactly match the exported live schema as the expected schema doesn't take into account any database wide settings or defaults.") + ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating the expected schema'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $schema = new Schema(); + + $this->applyMigrations('core', $schema); + + $apps = \OC_App::getEnabledApps(); + foreach ($apps as $app) { + $this->applyMigrations($app, $schema); + } + + $sql = $input->getOption('sql'); + if ($sql) { + $output->writeln($schema->toSql($this->connection->getDatabasePlatform())); + } else { + $encoder = new SchemaEncoder(); + $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform())); + } + + return 0; + } + + private function applyMigrations(string $app, Schema $schema): void { + $output = new NullOutput(); + $ms = new MigrationService($app, $this->connection, $output); + foreach ($ms->getAvailableVersions() as $version) { + $migration = $ms->createInstance($version); + $migration->changeSchema($output, function () use (&$schema) { + return new SchemaWrapper($this->connection, $schema); + }, []); + } + } +} diff --git a/core/Command/Db/ExportSchema.php b/core/Command/Db/ExportSchema.php new file mode 100644 index 00000000000..581824eea5f --- /dev/null +++ b/core/Command/Db/ExportSchema.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use OC\Core\Command\Base; +use OCP\IDBConnection; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ExportSchema extends Base { + public function __construct( + protected IDBConnection $connection, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('db:schema:export') + ->setDescription('Export the current database schema') + ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating a copy of the schema'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $schema = $this->connection->createSchema(); + $sql = $input->getOption('sql'); + if ($sql) { + $output->writeln($schema->toSql($this->connection->getDatabasePlatform())); + } else { + $encoder = new SchemaEncoder(); + $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform())); + } + + return 0; + } +} diff --git a/core/Command/Db/Migrations/ExecuteCommand.php b/core/Command/Db/Migrations/ExecuteCommand.php index c75b575ab6c..a89072c1ad1 100644 --- a/core/Command/Db/Migrations/ExecuteCommand.php +++ b/core/Command/Db/Migrations/ExecuteCommand.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php index 47f65b5a11b..a75280fa8b1 100644 --- a/core/Command/Db/Migrations/GenerateCommand.php +++ b/core/Command/Db/Migrations/GenerateCommand.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; @@ -33,37 +16,21 @@ use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareI use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class GenerateCommand extends Command implements CompletionAwareInterface { - protected static $_templateSimple = - '<?php + protected static $_templateSimple + = '<?php declare(strict_types=1); /** - * @copyright Copyright (c) {{year}} FIXME Your name <your@email.com> - * - * FIXME @author Your name <your@email.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/>. - * + * SPDX-FileCopyrightText: {{year}} Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace {{namespace}}; @@ -72,6 +39,7 @@ use Closure; use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; +use Override; /** * FIXME Auto-generated migration step: Please modify to your needs! @@ -83,6 +51,7 @@ class {{classname}} extends SimpleMigrationStep { * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ + #[Override] public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { } @@ -92,6 +61,7 @@ class {{classname}} extends SimpleMigrationStep { * @param array $options * @return null|ISchemaWrapper */ + #[Override] public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { {{schemabody}} } @@ -101,18 +71,16 @@ class {{classname}} extends SimpleMigrationStep { * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ + #[Override] public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { } } '; - protected Connection $connection; - protected IAppManager $appManager; - - public function __construct(Connection $connection, IAppManager $appManager) { - $this->connection = $connection; - $this->appManager = $appManager; - + public function __construct( + protected Connection $connection, + protected IAppManager $appManager, + ) { parent::__construct(); } @@ -147,7 +115,7 @@ class {{classname}} extends SimpleMigrationStep { if ($fullVersion) { [$major, $minor] = explode('.', $fullVersion); - $shouldVersion = (string) ((int)$major * 1000 + (int)$minor); + $shouldVersion = (string)((int)$major * 1000 + (int)$minor); if ($version !== $shouldVersion) { $output->writeln('<comment>Unexpected migration version for current version: ' . $fullVersion . '</comment>'); $output->writeln('<comment> - Pattern: XYYY </comment>'); @@ -155,6 +123,7 @@ class {{classname}} extends SimpleMigrationStep { $output->writeln('<comment> - Actual: ' . $version . '</comment>'); if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Continue with your given version? (y/n) [n] ', false); @@ -190,7 +159,7 @@ class {{classname}} extends SimpleMigrationStep { */ public function completeArgumentValues($argumentName, CompletionContext $context) { if ($argumentName === 'app') { - $allApps = \OC_App::getAllApps(); + $allApps = $this->appManager->getAllAppsInAppsFolders(); return array_diff($allApps, \OC_App::getEnabledApps(true, true)); } diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php new file mode 100644 index 00000000000..581259c99df --- /dev/null +++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Db\Migrations; + +use OC\Migration\MetadataManager; +use OCP\App\IAppManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @since 30.0.0 + */ +class GenerateMetadataCommand extends Command { + public function __construct( + private readonly MetadataManager $metadataManager, + private readonly IAppManager $appManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('migrations:generate-metadata') + ->setHidden(true) + ->setDescription('Generate metadata from DB migrations - internal and should not be used'); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln( + json_encode( + [ + 'migrations' => $this->extractMigrationMetadata() + ], + JSON_PRETTY_PRINT + ) + ); + + return 0; + } + + private function extractMigrationMetadata(): array { + return [ + 'core' => $this->extractMigrationMetadataFromCore(), + 'apps' => $this->extractMigrationMetadataFromApps() + ]; + } + + private function extractMigrationMetadataFromCore(): array { + return $this->metadataManager->extractMigrationAttributes('core'); + } + + /** + * get all apps and extract attributes + * + * @return array + * @throws \Exception + */ + private function extractMigrationMetadataFromApps(): array { + $allApps = $this->appManager->getAllAppsInAppsFolders(); + $metadata = []; + foreach ($allApps as $appId) { + // We need to load app before being able to extract Migrations + $alreadyLoaded = $this->appManager->isAppLoaded($appId); + if (!$alreadyLoaded) { + $this->appManager->loadApp($appId); + } + $metadata[$appId] = $this->metadataManager->extractMigrationAttributes($appId); + } + return $metadata; + } +} diff --git a/core/Command/Db/Migrations/MigrateCommand.php b/core/Command/Db/Migrations/MigrateCommand.php index 3e11b32665a..2e02f031479 100644 --- a/core/Command/Db/Migrations/MigrateCommand.php +++ b/core/Command/Db/Migrations/MigrateCommand.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php new file mode 100644 index 00000000000..f5b850fff76 --- /dev/null +++ b/core/Command/Db/Migrations/PreviewCommand.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Db\Migrations; + +use OC\Migration\MetadataManager; +use OC\Updater\ReleaseMetadata; +use OCP\Migration\Attributes\MigrationAttribute; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableCellStyle; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @since 30.0.0 + */ +class PreviewCommand extends Command { + private bool $initiated = false; + public function __construct( + private readonly MetadataManager $metadataManager, + private readonly ReleaseMetadata $releaseMetadata, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('migrations:preview') + ->setDescription('Get preview of available DB migrations in case of initiating an upgrade') + ->addArgument('version', InputArgument::REQUIRED, 'The destination version number'); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $version = $input->getArgument('version'); + if (filter_var($version, FILTER_VALIDATE_URL)) { + $metadata = $this->releaseMetadata->downloadMetadata($version); + } elseif (str_starts_with($version, '/')) { + $metadata = json_decode(file_get_contents($version), true, flags: JSON_THROW_ON_ERROR); + } else { + $metadata = $this->releaseMetadata->getMetadata($version); + } + + $parsed = $this->metadataManager->getMigrationsAttributesFromReleaseMetadata($metadata['migrations'] ?? [], true); + + $table = new Table($output); + $this->displayMigrations($table, 'core', $parsed['core'] ?? []); + foreach ($parsed['apps'] as $appId => $migrations) { + if (!empty($migrations)) { + $this->displayMigrations($table, $appId, $migrations); + } + } + $table->render(); + + $unsupportedApps = $this->metadataManager->getUnsupportedApps($metadata['migrations']); + if (!empty($unsupportedApps)) { + $output->writeln(''); + $output->writeln('Those apps are not supporting metadata yet and might initiate migrations on upgrade: <info>' . implode(', ', $unsupportedApps) . '</info>'); + } + + return 0; + } + + private function displayMigrations(Table $table, string $appId, array $data): void { + if (empty($data)) { + return; + } + + if ($this->initiated) { + $table->addRow(new TableSeparator()); + } + $this->initiated = true; + + $table->addRow( + [ + new TableCell( + $appId, + [ + 'colspan' => 2, + 'style' => new TableCellStyle(['cellFormat' => '<info>%s</info>']) + ] + ) + ] + )->addRow(new TableSeparator()); + + /** @var MigrationAttribute[] $attributes */ + foreach ($data as $migration => $attributes) { + $attributesStr = []; + if (empty($attributes)) { + $attributesStr[] = '<comment>(metadata not set)</comment>'; + } + foreach ($attributes as $attribute) { + $definition = '<info>' . $attribute->definition() . '</info>'; + $definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription(); + $definition .= empty($attribute->getNotes()) ? '' : "\n <comment>" . implode("</comment>\n <comment>", $attribute->getNotes()) . '</comment>'; + $attributesStr[] = $definition; + } + $table->addRow([$migration, implode("\n", $attributesStr)]); + } + } +} diff --git a/core/Command/Db/Migrations/StatusCommand.php b/core/Command/Db/Migrations/StatusCommand.php index 52bc51a169f..97ecc76a924 100644 --- a/core/Command/Db/Migrations/StatusCommand.php +++ b/core/Command/Db/Migrations/StatusCommand.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @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/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/SchemaEncoder.php b/core/Command/Db/SchemaEncoder.php new file mode 100644 index 00000000000..beae3a81264 --- /dev/null +++ b/core/Command/Db/SchemaEncoder.php @@ -0,0 +1,115 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\PhpIntegerMappingType; +use Doctrine\DBAL\Types\Type; + +class SchemaEncoder { + /** + * Encode a DBAL schema to json, performing some normalization based on the database platform + * + * @param Schema $schema + * @param AbstractPlatform $platform + * @return array + */ + public function encodeSchema(Schema $schema, AbstractPlatform $platform): array { + $encoded = ['tables' => [], 'sequences' => []]; + foreach ($schema->getTables() as $table) { + $encoded[$table->getName()] = $this->encodeTable($table, $platform); + } + ksort($encoded); + return $encoded; + } + + /** + * @psalm-type ColumnArrayType = + */ + private function encodeTable(Table $table, AbstractPlatform $platform): array { + $encoded = ['columns' => [], 'indexes' => []]; + foreach ($table->getColumns() as $column) { + /** + * @var array{ + * name: string, + * default: mixed, + * notnull: bool, + * length: ?int, + * precision: int, + * scale: int, + * unsigned: bool, + * fixed: bool, + * autoincrement: bool, + * comment: string, + * columnDefinition: ?string, + * collation?: string, + * charset?: string, + * jsonb?: bool, + * } $data + **/ + $data = $column->toArray(); + $data['type'] = Type::getTypeRegistry()->lookupName($column->getType()); + $data['default'] = $column->getType()->convertToPHPValue($column->getDefault(), $platform); + if ($platform instanceof PostgreSQLPlatform) { + $data['unsigned'] = false; + if ($column->getType() instanceof PhpIntegerMappingType) { + $data['length'] = null; + } + unset($data['jsonb']); + } elseif ($platform instanceof AbstractMySqlPlatform) { + if ($column->getType() instanceof PhpIntegerMappingType) { + $data['length'] = null; + } elseif (in_array($data['type'], ['text', 'blob', 'datetime', 'float', 'json'])) { + $data['length'] = 0; + } + unset($data['collation']); + unset($data['charset']); + } + if ($data['type'] === 'string' && $data['length'] === null) { + $data['length'] = 255; + } + $encoded['columns'][$column->getName()] = $data; + } + ksort($encoded['columns']); + foreach ($table->getIndexes() as $index) { + $options = $index->getOptions(); + if (isset($options['lengths']) && count(array_filter($options['lengths'])) === 0) { + unset($options['lengths']); + } + if ($index->isPrimary()) { + if ($platform instanceof PostgreSqlPlatform) { + $name = $table->getName() . '_pkey'; + } elseif ($platform instanceof AbstractMySQLPlatform) { + $name = 'PRIMARY'; + } else { + $name = $index->getName(); + } + } else { + $name = $index->getName(); + } + if ($platform instanceof PostgreSqlPlatform) { + $name = strtolower($name); + } + $encoded['indexes'][$name] = [ + 'name' => $name, + 'columns' => $index->getColumns(), + 'unique' => $index->isUnique(), + 'primary' => $index->isPrimary(), + 'flags' => $index->getFlags(), + 'options' => $options, + ]; + } + ksort($encoded['indexes']); + return $encoded; + } +} |