diff options
Diffstat (limited to 'core/Command/Db')
-rw-r--r-- | core/Command/Db/AddMissingColumns.php | 92 | ||||
-rw-r--r-- | core/Command/Db/AddMissingIndices.php | 467 | ||||
-rw-r--r-- | core/Command/Db/AddMissingPrimaryKeys.php | 175 | ||||
-rw-r--r-- | core/Command/Db/ConvertFilecacheBigInt.php | 54 | ||||
-rw-r--r-- | core/Command/Db/ConvertMysqlToMB4.php | 48 | ||||
-rw-r--r-- | core/Command/Db/ConvertType.php | 98 | ||||
-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 | 35 | ||||
-rw-r--r-- | core/Command/Db/Migrations/GenerateCommand.php | 77 | ||||
-rw-r--r-- | core/Command/Db/Migrations/GenerateMetadataCommand.php | 79 | ||||
-rw-r--r-- | core/Command/Db/Migrations/MigrateCommand.php | 30 | ||||
-rw-r--r-- | core/Command/Db/Migrations/PreviewCommand.php | 111 | ||||
-rw-r--r-- | core/Command/Db/Migrations/StatusCommand.php | 31 | ||||
-rw-r--r-- | core/Command/Db/SchemaEncoder.php | 115 |
15 files changed, 666 insertions, 858 deletions
diff --git a/core/Command/Db/AddMissingColumns.php b/core/Command/Db/AddMissingColumns.php index acc05c3b7ff..33b4b24a6cb 100644 --- a/core/Command/Db/AddMissingColumns.php +++ b/core/Command/Db/AddMissingColumns.php @@ -3,38 +3,19 @@ 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; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingColumnsEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingColumns @@ -45,64 +26,53 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingColumns extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { $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 { - $this->addCoreColumns($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update columns if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_COLUMNS_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCoreColumns(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check columns of the comments table.</info>'); - - $schema = new SchemaWrapper($this->connection); + $event = new AddMissingColumnsEvent(); + $this->dispatcher->dispatchTyped($event); + $missingColumns = $event->getMissingColumns(); $updated = false; - if ($schema->hasTable('comments')) { - $table = $schema->getTable('comments'); - if (!$table->hasColumn('reference_id')) { - $output->writeln('<info>Adding additional reference_id column to the comments table, this can take some time...</info>'); - $table->addColumn('reference_id', 'string', [ - 'notnull' => false, - 'length' => 64, - ]); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); + if (!empty($missingColumns)) { + $schema = new SchemaWrapper($this->connection); + + foreach ($missingColumns as $missingColumn) { + if ($schema->hasTable($missingColumn['tableName'])) { + $table = $schema->getTable($missingColumn['tableName']); + if (!$table->hasColumn($missingColumn['columnName'])) { + $output->writeln('<info>Adding additional ' . $missingColumn['columnName'] . ' column to the ' . $missingColumn['tableName'] . ' table, this can take some time...</info>'); + $table->addColumn($missingColumn['columnName'], $missingColumn['typeName'], $missingColumn['options']); + $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); + if ($dryRun && $sqlQueries !== null) { + $output->writeln($sqlQueries); + } + $updated = true; + $output->writeln('<info>' . $missingColumn['tableName'] . ' table updated successfully.</info>'); + } } - $updated = true; - $output->writeln('<info>Comments table updated successfully.</info>'); } } if (!$updated) { $output->writeln('<info>Done.</info>'); } + + return 0; } } diff --git a/core/Command/Db/AddMissingIndices.php b/core/Command/Db/AddMissingIndices.php index 5799a462ffa..eec0aedce11 100644 --- a/core/Command/Db/AddMissingIndices.php +++ b/core/Command/Db/AddMissingIndices.php @@ -3,46 +3,19 @@ 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; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingIndices @@ -53,412 +26,104 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingIndices extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { $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 { - $this->addCoreIndexes($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update indexes if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_INDEXES_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCoreIndexes(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check indices of the share table.</info>'); - - $schema = new SchemaWrapper($this->connection); - $updated = false; - - if ($schema->hasTable('share')) { - $table = $schema->getTable('share'); - if (!$table->hasIndex('share_with_index')) { - $output->writeln('<info>Adding additional share_with index to the share table, this can take some time...</info>'); - $table->addIndex(['share_with'], 'share_with_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('parent_index')) { - $output->writeln('<info>Adding additional parent index to the share table, this can take some time...</info>'); - $table->addIndex(['parent'], 'parent_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('owner_index')) { - $output->writeln('<info>Adding additional owner index to the share table, this can take some time...</info>'); - $table->addIndex(['uid_owner'], 'owner_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('initiator_index')) { - $output->writeln('<info>Adding additional initiator index to the share table, this can take some time...</info>'); - $table->addIndex(['uid_initiator'], 'initiator_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the filecache table.</info>'); - if ($schema->hasTable('filecache')) { - $table = $schema->getTable('filecache'); - if (!$table->hasIndex('fs_mtime')) { - $output->writeln('<info>Adding additional mtime index to the filecache table, this can take some time...</info>'); - $table->addIndex(['mtime'], 'fs_mtime'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_size')) { - $output->writeln('<info>Adding additional size index to the filecache table, this can take some time...</info>'); - $table->addIndex(['size'], 'fs_size'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_id_storage_size')) { - $output->writeln('<info>Adding additional size index to the filecache table, this can take some time...</info>'); - $table->addIndex(['fileid', 'storage', 'size'], 'fs_id_storage_size'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_storage_path_prefix') && !$schema->getDatabasePlatform() instanceof PostgreSQL94Platform) { - $output->writeln('<info>Adding additional path index to the filecache table, this can take some time...</info>'); - $table->addIndex(['storage', 'path'], 'fs_storage_path_prefix', [], ['lengths' => [null, 64]]); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the twofactor_providers table.</info>'); - if ($schema->hasTable('twofactor_providers')) { - $table = $schema->getTable('twofactor_providers'); - if (!$table->hasIndex('twofactor_providers_uid')) { - $output->writeln('<info>Adding additional twofactor_providers_uid index to the twofactor_providers table, this can take some time...</info>'); - $table->addIndex(['uid'], 'twofactor_providers_uid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Twofactor_providers table updated successfully.</info>'); - } - } + $event = new AddMissingIndicesEvent(); + $this->dispatcher->dispatchTyped($event); + + $missingIndices = $event->getMissingIndices(); + $toReplaceIndices = $event->getIndicesToReplace(); + + if ($missingIndices !== [] || $toReplaceIndices !== []) { + $schema = new SchemaWrapper($this->connection); + + foreach ($missingIndices as $missingIndex) { + if ($schema->hasTable($missingIndex['tableName'])) { + $table = $schema->getTable($missingIndex['tableName']); + if (!$table->hasIndex($missingIndex['indexName'])) { + $output->writeln('<info>Adding additional ' . $missingIndex['indexName'] . ' index to the ' . $table->getName() . ' table, this can take some time...</info>'); + + if ($missingIndex['dropUnnamedIndex']) { + foreach ($table->getIndexes() as $index) { + $columns = $index->getColumns(); + if ($columns === $missingIndex['columns']) { + $table->dropIndex($index->getName()); + } + } + } - $output->writeln('<info>Check indices of the login_flow_v2 table.</info>'); - if ($schema->hasTable('login_flow_v2')) { - $table = $schema->getTable('login_flow_v2'); - if (!$table->hasIndex('poll_token')) { - $output->writeln('<info>Adding additional indeces to the login_flow_v2 table, this can take some time...</info>'); + if ($missingIndex['uniqueIndex']) { + $table->addUniqueIndex($missingIndex['columns'], $missingIndex['indexName'], $missingIndex['options']); + } else { + $table->addIndex($missingIndex['columns'], $missingIndex['indexName'], [], $missingIndex['options']); + } - foreach ($table->getIndexes() as $index) { - $columns = $index->getColumns(); - if ($columns === ['poll_token'] || - $columns === ['login_token'] || - $columns === ['timestamp']) { - $table->dropIndex($index->getName()); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); + } + $output->writeln('<info>' . $table->getName() . ' table updated successfully.</info>'); } } - - $table->addUniqueIndex(['poll_token'], 'poll_token'); - $table->addUniqueIndex(['login_token'], 'login_token'); - $table->addIndex(['timestamp'], 'timestamp'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>login_flow_v2 table updated successfully.</info>'); } - } - $output->writeln('<info>Check indices of the whats_new table.</info>'); - if ($schema->hasTable('whats_new')) { - $table = $schema->getTable('whats_new'); - if (!$table->hasIndex('version')) { - $output->writeln('<info>Adding version index to the whats_new table, this can take some time...</info>'); + foreach ($toReplaceIndices as $toReplaceIndex) { + if ($schema->hasTable($toReplaceIndex['tableName'])) { + $table = $schema->getTable($toReplaceIndex['tableName']); - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['version']) { - $table->dropIndex($index->getName()); + if ($table->hasIndex($toReplaceIndex['newIndexName'])) { + continue; } - } - $table->addUniqueIndex(['version'], 'version'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>whats_new table updated successfully.</info>'); - } - } + $output->writeln('<info>Adding additional ' . $toReplaceIndex['newIndexName'] . ' index to the ' . $table->getName() . ' table, this can take some time...</info>'); - $output->writeln('<info>Check indices of the cards table.</info>'); - $cardsUpdated = false; - if ($schema->hasTable('cards')) { - $table = $schema->getTable('cards'); - - if ($table->hasIndex('addressbookid_uri_index')) { - if ($table->hasIndex('cards_abiduri')) { - $table->dropIndex('addressbookid_uri_index'); - } else { - $output->writeln('<info>Renaming addressbookid_uri_index index to cards_abiduri in the cards table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid', 'uri']) { - $table->renameIndex('addressbookid_uri_index', 'cards_abiduri'); - } + if ($toReplaceIndex['uniqueIndex']) { + $table->addUniqueIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], $toReplaceIndex['options']); + } else { + $table->addIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], [], $toReplaceIndex['options']); } - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } - - if (!$table->hasIndex('cards_abid')) { - $output->writeln('<info>Adding cards_abid index to the cards table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid']) { - $table->dropIndex($index->getName()); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); } - } - $table->addIndex(['addressbookid'], 'cards_abid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } - - if (!$table->hasIndex('cards_abiduri')) { - $output->writeln('<info>Adding cards_abiduri index to the cards table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid', 'uri']) { - $table->dropIndex($index->getName()); + foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { + if ($table->hasIndex($oldIndexName)) { + $output->writeln('<info>Removing ' . $oldIndexName . ' index from the ' . $table->getName() . ' table</info>'); + $table->dropIndex($oldIndexName); + } } - } - - $table->addIndex(['addressbookid', 'uri'], 'cards_abiduri'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } - - if ($cardsUpdated) { - $updated = true; - $output->writeln('<info>cards table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the cards_properties table.</info>'); - if ($schema->hasTable('cards_properties')) { - $table = $schema->getTable('cards_properties'); - if (!$table->hasIndex('cards_prop_abid')) { - $output->writeln('<info>Adding cards_prop_abid index to the cards_properties table, this can take some time...</info>'); - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid']) { - $table->dropIndex($index->getName()); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); } + $output->writeln('<info>' . $table->getName() . ' table updated successfully.</info>'); } - - $table->addIndex(['addressbookid'], 'cards_prop_abid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>cards_properties table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the calendarobjects_props table.</info>'); - if ($schema->hasTable('calendarobjects_props')) { - $table = $schema->getTable('calendarobjects_props'); - if (!$table->hasIndex('calendarobject_calid_index')) { - $output->writeln('<info>Adding calendarobject_calid_index index to the calendarobjects_props table, this can take some time...</info>'); - - $table->addIndex(['calendarid', 'calendartype'], 'calendarobject_calid_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>calendarobjects_props table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the schedulingobjects table.</info>'); - if ($schema->hasTable('schedulingobjects')) { - $table = $schema->getTable('schedulingobjects'); - if (!$table->hasIndex('schedulobj_principuri_index')) { - $output->writeln('<info>Adding schedulobj_principuri_index index to the schedulingobjects table, this can take some time...</info>'); - - $table->addIndex(['principaluri'], 'schedulobj_principuri_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>schedulingobjects table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_properties table.</info>'); - if ($schema->hasTable('properties')) { - $table = $schema->getTable('properties'); - $propertiesUpdated = false; - - if (!$table->hasIndex('properties_path_index')) { - $output->writeln('<info>Adding properties_path_index index to the oc_properties table, this can take some time...</info>'); - - $table->addIndex(['userid', 'propertypath'], 'properties_path_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $propertiesUpdated = true; - } - if (!$table->hasIndex('properties_pathonly_index')) { - $output->writeln('<info>Adding properties_pathonly_index index to the oc_properties table, this can take some time...</info>'); - - $table->addIndex(['propertypath'], 'properties_pathonly_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $propertiesUpdated = true; - } - - if ($propertiesUpdated) { - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_jobs table.</info>'); - if ($schema->hasTable('jobs')) { - $table = $schema->getTable('jobs'); - if (!$table->hasIndex('job_lastcheck_reserved')) { - $output->writeln('<info>Adding job_lastcheck_reserved index to the oc_jobs table, this can take some time...</info>'); - - $table->addIndex(['last_checked', 'reserved_at'], 'job_lastcheck_reserved'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); } - } - $output->writeln('<info>Check indices of the oc_direct_edit table.</info>'); - if ($schema->hasTable('direct_edit')) { - $table = $schema->getTable('direct_edit'); - if (!$table->hasIndex('direct_edit_timestamp')) { - $output->writeln('<info>Adding direct_edit_timestamp index to the oc_direct_edit table, this can take some time...</info>'); - - $table->addIndex(['timestamp'], 'direct_edit_timestamp'); + if ($dryRun) { $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { + if ($sqlQueries !== null) { $output->writeln($sqlQueries); } - $updated = true; - $output->writeln('<info>oc_direct_edit table updated successfully.</info>'); } } - $output->writeln('<info>Check indices of the oc_preferences table.</info>'); - if ($schema->hasTable('preferences')) { - $table = $schema->getTable('preferences'); - if (!$table->hasIndex('preferences_app_key')) { - $output->writeln('<info>Adding preferences_app_key index to the oc_preferences table, this can take some time...</info>'); - - $table->addIndex(['appid', 'configkey'], 'preferences_app_key'); - $this->connection->migrateToSchema($schema->getWrappedSchema()); - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_mounts table.</info>'); - if ($schema->hasTable('mounts')) { - $table = $schema->getTable('mounts'); - if (!$table->hasIndex('mounts_class_index')) { - $output->writeln('<info>Adding mounts_class_index index to the oc_mounts table, this can take some time...</info>'); - - $table->addIndex(['mount_provider_class'], 'mounts_class_index'); - $this->connection->migrateToSchema($schema->getWrappedSchema()); - $updated = true; - $output->writeln('<info>oc_mounts table updated successfully.</info>'); - } - } - - if (!$updated) { - $output->writeln('<info>Done.</info>'); - } + return 0; } } diff --git a/core/Command/Db/AddMissingPrimaryKeys.php b/core/Command/Db/AddMissingPrimaryKeys.php index 8262cf37e77..1eb11c894fa 100644 --- a/core/Command/Db/AddMissingPrimaryKeys.php +++ b/core/Command/Db/AddMissingPrimaryKeys.php @@ -3,38 +3,19 @@ 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; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingPrimaryKeyEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingPrimaryKeys @@ -45,149 +26,59 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingPrimaryKeys extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { $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 { - $this->addCorePrimaryKeys($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update indexes if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_PRIMARY_KEYS_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCorePrimaryKeys(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check primary keys.</info>'); - - $schema = new SchemaWrapper($this->connection); + $event = new AddMissingPrimaryKeyEvent(); + $this->dispatcher->dispatchTyped($event); + $missingKeys = $event->getMissingPrimaryKeys(); $updated = false; - if ($schema->hasTable('federated_reshares')) { - $table = $schema->getTable('federated_reshares'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the federated_reshares table, this can take some time...</info>'); - $table->setPrimaryKey(['share_id'], 'federated_res_pk'); - if ($table->hasIndex('share_id_index')) { - $table->dropIndex('share_id_index'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>federated_reshares table updated successfully.</info>'); - } - } - - if ($schema->hasTable('systemtag_object_mapping')) { - $table = $schema->getTable('systemtag_object_mapping'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the systemtag_object_mapping table, this can take some time...</info>'); - $table->setPrimaryKey(['objecttype', 'objectid', 'systemtagid'], 'som_pk'); - if ($table->hasIndex('mapping')) { - $table->dropIndex('mapping'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>systemtag_object_mapping table updated successfully.</info>'); - } - } + if (!empty($missingKeys)) { + $schema = new SchemaWrapper($this->connection); - if ($schema->hasTable('comments_read_markers')) { - $table = $schema->getTable('comments_read_markers'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the comments_read_markers table, this can take some time...</info>'); - $table->setPrimaryKey(['user_id', 'object_type', 'object_id'], 'crm_pk'); - if ($table->hasIndex('comments_marker_index')) { - $table->dropIndex('comments_marker_index'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>comments_read_markers table updated successfully.</info>'); - } - } + foreach ($missingKeys as $missingKey) { + if ($schema->hasTable($missingKey['tableName'])) { + $table = $schema->getTable($missingKey['tableName']); + if (!$table->hasPrimaryKey()) { + $output->writeln('<info>Adding primary key to the ' . $missingKey['tableName'] . ' table, this can take some time...</info>'); + $table->setPrimaryKey($missingKey['columns'], $missingKey['primaryKeyName']); - if ($schema->hasTable('collres_resources')) { - $table = $schema->getTable('collres_resources'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the collres_resources table, this can take some time...</info>'); - $table->setPrimaryKey(['collection_id', 'resource_type', 'resource_id'], 'crr_pk'); - if ($table->hasIndex('collres_unique_res')) { - $table->dropIndex('collres_unique_res'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>collres_resources table updated successfully.</info>'); - } - } + if ($missingKey['formerIndex'] && $table->hasIndex($missingKey['formerIndex'])) { + $table->dropIndex($missingKey['formerIndex']); + } - if ($schema->hasTable('collres_accesscache')) { - $table = $schema->getTable('collres_accesscache'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the collres_accesscache table, this can take some time...</info>'); - $table->setPrimaryKey(['user_id', 'collection_id', 'resource_type', 'resource_id'], 'cra_pk'); - if ($table->hasIndex('collres_unique_user')) { - $table->dropIndex('collres_unique_user'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>collres_accesscache table updated successfully.</info>'); - } - } + $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); + if ($dryRun && $sqlQueries !== null) { + $output->writeln($sqlQueries); + } - if ($schema->hasTable('filecache_extended')) { - $table = $schema->getTable('filecache_extended'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the filecache_extended table, this can take some time...</info>'); - $table->setPrimaryKey(['fileid'], 'fce_pk'); - if ($table->hasIndex('fce_fileid_idx')) { - $table->dropIndex('fce_fileid_idx'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); + $updated = true; + $output->writeln('<info>' . $missingKey['tableName'] . ' table updated successfully.</info>'); + } } - $updated = true; - $output->writeln('<info>filecache_extended table updated successfully.</info>'); } } if (!$updated) { $output->writeln('<info>Done.</info>'); } + + return 0; } } diff --git a/core/Command/Db/ConvertFilecacheBigInt.php b/core/Command/Db/ConvertFilecacheBigInt.php index 9d77ac9a5a0..0d96d139701 100644 --- a/core/Command/Db/ConvertFilecacheBigInt.php +++ b/core/Command/Db/ConvertFilecacheBigInt.php @@ -1,51 +1,26 @@ <?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 OCP\DB\Types; 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; class ConvertFilecacheBigInt extends Command { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } @@ -55,8 +30,10 @@ class ConvertFilecacheBigInt extends Command { ->setDescription('Convert the ID columns of the filecache to BigInt'); } - protected function getColumnsByTable() { - // also update in CheckSetupController::hasBigIntConversionPendingColumns() + /** + * @return array<string,string[]> + */ + public static function getColumnsByTable(): array { return [ 'activity' => ['activity_id', 'object_id'], 'activity_mq' => ['mail_id'], @@ -78,10 +55,10 @@ 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 = $this->getColumnsByTable(); + $tables = static::getColumnsByTable(); foreach ($tables as $tableName => $columns) { if (!$schema->hasTable($tableName)) { continue; @@ -114,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 19a9532d910..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; @@ -37,21 +18,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ConvertMysqlToMB4 extends Command { - private IConfig $config; - private IDBConnection $connection; - private IURLGenerator $urlGenerator; - private LoggerInterface $logger; - public function __construct( - IConfig $config, - IDBConnection $connection, - IURLGenerator $urlGenerator, - LoggerInterface $logger + private IConfig $config, + private IDBConnection $connection, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, ) { - $this->config = $config; - $this->connection = $connection; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; parent::__construct(); } @@ -62,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 f7638e3024f..0067bec4d9e 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -1,46 +1,24 @@ <?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; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Schema\AbstractAsset; use Doctrine\DBAL\Schema\Table; -use OCP\DB\Types; 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; @@ -56,13 +34,13 @@ use function preg_match; use function preg_quote; class ConvertType extends Command implements CompletionAwareInterface { - protected IConfig $config; - protected ConnectionFactory $connectionFactory; - protected array $columnTypes; + protected array $columnTypes = []; - public function __construct(IConfig $config, ConnectionFactory $connectionFactory) { - $this->config = $config; - $this->connectionFactory = $connectionFactory; + public function __construct( + protected IConfig $config, + protected ConnectionFactory $connectionFactory, + protected IAppManager $appManager, + ) { parent::__construct(); } @@ -168,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; } @@ -182,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')) { @@ -200,7 +181,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $output->writeln('<comment>The following tables will not be converted:</comment>'); $output->writeln($extraFromTables); if (!$input->getOption('all-apps')) { - $output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>'); + $output->writeln('<comment>Please note that tables belonging to disabled (but not removed) apps</comment>'); $output->writeln('<comment>can be included by specifying the --all-apps option.</comment>'); } @@ -229,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') { @@ -245,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); } @@ -264,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); } } @@ -277,7 +275,7 @@ class ConvertType extends Command implements CompletionAwareInterface { } return preg_match($filterExpression, $asset) !== false; }); - return $db->getSchemaManager()->listTableNames(); + return $db->createSchemaManager()->listTableNames(); } /** @@ -299,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(); @@ -338,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(); @@ -405,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 @@ -428,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 e87e133fa31..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; @@ -35,13 +19,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ExecuteCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - private IConfig $config; - - public function __construct(Connection $connection, IConfig $config) { - $this->connection = $connection; - $this->config = $config; - + public function __construct( + private Connection $connection, + private IConfig $config, + ) { parent::__construct(); } diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php index aa93adaebb4..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}} Your name <your@email.com> - * - * @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,9 +39,10 @@ use Closure; use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; +use Override; /** - * Auto-generated migration step: Please modify to your needs! + * FIXME Auto-generated migration step: Please modify to your needs! */ class {{classname}} extends SimpleMigrationStep { @@ -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}} } @@ -99,20 +69,18 @@ class {{classname}} extends SimpleMigrationStep { /** * @param IOutput $output * @param Closure(): ISchemaWrapper $schemaClosure -g * @param array $options + * @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 @@ g * @param array $options 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 @@ g * @param array $options $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 @@ g * @param array $options */ 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)); } @@ -235,7 +204,7 @@ g * @param array $options $path = $dir . '/' . $className . '.php'; if (file_put_contents($path, $code) === false) { - throw new RuntimeException('Failed to generate new migration step.'); + throw new RuntimeException('Failed to generate new migration step. Could not write to ' . $path); } return $path; 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 f0f35716997..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; @@ -33,10 +18,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class MigrateCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } 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 725ee075215..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; @@ -34,10 +18,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class StatusCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } 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; + } +} |