Skip to content

Commit

Permalink
feat(upgrade): migration attributes
Browse files Browse the repository at this point in the history
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>

d

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>

f

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>

d

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
  • Loading branch information
ArtificialOwl committed Jul 17, 2024
1 parent b06ce83 commit c11cae7
Show file tree
Hide file tree
Showing 20 changed files with 778 additions and 0 deletions.
106 changes: 106 additions & 0 deletions core/Command/Db/Migrations/GenerateMetadataCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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\DB\Connection;
use OC\DB\MigrationService;
use OCP\App\IAppManager;
use ReflectionClass;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GenerateMetadataCommand extends Command {
public function __construct(
private readonly Connection $connection,
private readonly IAppManager $appManager,
) {
parent::__construct();
}

protected function configure() {

Check notice

Code scanning / Psalm

MissingReturnType Note

Method OC\Core\Command\Db\Migrations\GenerateMetadataCommand::configure does not have a return type, expecting 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->extractMigrationAttributes('core');
}

/**
* get all apps and extract attributes
*
* @return array
* @throws \Exception
*/
private function extractMigrationMetadataFromApps(): array {
$allApps = \OC_App::getAllApps();
$metadata = [];
foreach ($allApps as $appId) {
// We need to load app before being able to extract Migrations
// If app was not enabled before, we will disable it afterward.
$alreadyLoaded = $this->appManager->isInstalled($appId);
if (!$alreadyLoaded) {
$this->appManager->loadApp($appId);
}
$metadata[$appId] = $this->extractMigrationAttributes($appId);
if (!$alreadyLoaded) {
$this->appManager->disableApp($appId);
}
}
return $metadata;
}

/**
* We get all migrations from an app, and for each migration we extract attributes
*
* @param string $appId
*
* @return array
* @throws \Exception
*/
private function extractMigrationAttributes(string $appId): array {
$ms = new MigrationService($appId, $this->connection);

$metadata = [];
foreach($ms->getAvailableVersions() as $version) {
$metadata[$version] = [];
$class = new ReflectionClass($ms->createInstance($version));
$attributes = $class->getAttributes();
foreach ($attributes as $attribute) {
$metadata[$version][] = $attribute->newInstance();
}
}

return $metadata;
}
}
162 changes: 162 additions & 0 deletions core/Command/Db/Migrations/PreviewCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?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\DB\Connection;
use OC\DB\MigrationService;
use OCP\Migration\Attributes\GenericMigrationAttribute;
use OCP\Migration\Attributes\MigrationAttribute;
use OCP\Migration\Exceptions\AttributeException;
use Psr\Log\LoggerInterface;
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;

class PreviewCommand extends Command {
public function __construct(
private readonly Connection $connection,
private readonly LoggerInterface $logger,
) {
parent::__construct();
}

protected function configure() {

Check notice

Code scanning / Psalm

MissingReturnType Note

Method OC\Core\Command\Db\Migrations\PreviewCommand::configure does not have a return type, expecting 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');

$metadata = $this->getMetadata($version);
$parsed = $this->getMigrationsAttributes($metadata);

$table = new Table($output);
$this->displayMigrations($table, 'core', $parsed['core']);

$table->render();

return 0;
}

private function displayMigrations(Table $table, string $appId, array $data): void {
$done = $this->getDoneMigrations($appId);
$done = array_diff($done, ['30000Date20240429122720']);

$table->addRow(
[
new TableCell(
$appId,
[
'colspan' => 2,
'style' => new TableCellStyle(['cellFormat' => '<info>%s</info>'])
]
)
]
)->addRow(new TableSeparator());

foreach($data as $migration => $attributes) {
if (in_array($migration, $done)) {
continue;
}

$attributesStr = [];
/** @var MigrationAttribute[] $attributes */
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)]);
}

}





private function getMetadata(string $version): array {
$metadata = json_decode(file_get_contents('/tmp/nextcloud-' . $version . '.metadata'), true);
if (!$metadata) {
throw new \Exception();
}
return $metadata['migrations'] ?? [];
}

private function getDoneMigrations(string $appId): array {
$ms = new MigrationService($appId, $this->connection);
return $ms->getMigratedVersions();
}

private function getMigrationsAttributes(array $metadata): array {
$appsAttributes = [];
foreach (array_keys($metadata['apps']) as $appId) {
$appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? []);

Check failure on line 111 in core/Command/Db/Migrations/PreviewCommand.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidArrayOffset

core/Command/Db/Migrations/PreviewCommand.php:111:4: InvalidArrayOffset: Cannot access value on variable $appsAttributes[$appId] using a key-of<array<array-key, mixed>> offset, expecting array-key (see https://psalm.dev/115)

Check failure

Code scanning / Psalm

InvalidArrayOffset Error

Cannot access value on variable $appsAttributes[$appId] using a key-of<array<array-key, mixed>> offset, expecting array-key
}

return [
'core' => $this->parseMigrations($metadata['core'] ?? []),
'apps' => $appsAttributes
];
}

private function parseMigrations(array $migrations): array {
$parsed = [];
foreach (array_keys($migrations) as $entry) {
$items = $migrations[$entry];
$parsed[$entry] = [];
foreach ($items as $item) {
try {
$parsed[$entry][] = $this->createAttribute($item);
} catch (AttributeException $e) {
$this->logger->warning(
'exception while trying to create attribute',
['exception' => $e, 'item' => json_encode($item)]
);
$parsed[$entry][] = new GenericMigrationAttribute($item);
}
}
}

return $parsed;
}

/**
* @param array $item
*
* @return MigrationAttribute|null
* @throws AttributeException
*/
private function createAttribute(array $item): ?MigrationAttribute {
$class = $item['class'] ?? '';
$namespace = 'OCP\Migration\Attributes\\';
if (!str_starts_with($class, $namespace)

Check notice

Code scanning / Psalm

RedundantConditionGivenDocblockType Note

Operand of type true is always truthy
|| !ctype_alpha(substr($class, strlen($namespace)))) {
throw new AttributeException('class name does not looks valid');
}

try {
$attribute = new $class();

Check notice

Code scanning / Psalm

InvalidStringClass Note

String cannot be used as a class
return $attribute->import($item);
} catch (\Error) {
throw new AttributeException('cannot import Attribute');
}
}
}
2 changes: 2 additions & 0 deletions core/register_command.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
$application->add(Server::get(Command\Db\ExpectedSchema::class));
$application->add(Server::get(Command\Db\ExportSchema::class));

$application->add(Server::get(Command\Db\Migrations\GenerateMetadataCommand::class));
$application->add(Server::get(Command\Db\Migrations\PreviewCommand::class));
if ($config->getSystemValueBool('debug', false)) {
$application->add(Server::get(Command\Db\Migrations\StatusCommand::class));
$application->add(Server::get(Command\Db\Migrations\MigrateCommand::class));
Expand Down
17 changes: 17 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,22 @@
'OCP\\Mail\\IEMailTemplate' => $baseDir . '/lib/public/Mail/IEMailTemplate.php',
'OCP\\Mail\\IMailer' => $baseDir . '/lib/public/Mail/IMailer.php',
'OCP\\Mail\\IMessage' => $baseDir . '/lib/public/Mail/IMessage.php',
'OCP\\Migration\\Attributes\\AddColumn' => $baseDir . '/lib/public/Migration/Attributes/AddColumn.php',
'OCP\\Migration\\Attributes\\AddIndex' => $baseDir . '/lib/public/Migration/Attributes/AddIndex.php',
'OCP\\Migration\\Attributes\\ColumnMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/ColumnMigrationAttribute.php',
'OCP\\Migration\\Attributes\\ColumnType' => $baseDir . '/lib/public/Migration/Attributes/ColumnType.php',
'OCP\\Migration\\Attributes\\CreateTable' => $baseDir . '/lib/public/Migration/Attributes/CreateTable.php',
'OCP\\Migration\\Attributes\\DeleteTable' => $baseDir . '/lib/public/Migration/Attributes/DeleteTable.php',
'OCP\\Migration\\Attributes\\DropColumn' => $baseDir . '/lib/public/Migration/Attributes/DropColumn.php',
'OCP\\Migration\\Attributes\\DropIndex' => $baseDir . '/lib/public/Migration/Attributes/DropIndex.php',
'OCP\\Migration\\Attributes\\GenericMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/GenericMigrationAttribute.php',
'OCP\\Migration\\Attributes\\IndexMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/IndexMigrationAttribute.php',
'OCP\\Migration\\Attributes\\IndexType' => $baseDir . '/lib/public/Migration/Attributes/IndexType.php',
'OCP\\Migration\\Attributes\\MigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/MigrationAttribute.php',
'OCP\\Migration\\Attributes\\ModifyColumn' => $baseDir . '/lib/public/Migration/Attributes/ModifyColumn.php',
'OCP\\Migration\\Attributes\\TableMigrationAttribute' => $baseDir . '/lib/public/Migration/Attributes/TableMigrationAttribute.php',
'OCP\\Migration\\BigIntMigration' => $baseDir . '/lib/public/Migration/BigIntMigration.php',
'OCP\\Migration\\Exceptions\\AttributeException' => $baseDir . '/lib/public/Migration/Exceptions/AttributeException.php',
'OCP\\Migration\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php',
'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php',
'OCP\\Migration\\IRepairStep' => $baseDir . '/lib/public/Migration/IRepairStep.php',
Expand Down Expand Up @@ -1118,7 +1133,9 @@
'OC\\Core\\Command\\Db\\ExportSchema' => $baseDir . '/core/Command/Db/ExportSchema.php',
'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => $baseDir . '/core/Command/Db/Migrations/ExecuteCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\GenerateMetadataCommand' => $baseDir . '/core/Command/Db/Migrations/GenerateMetadataCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => $baseDir . '/core/Command/Db/Migrations/MigrateCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\PreviewCommand' => $baseDir . '/core/Command/Db/Migrations/PreviewCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => $baseDir . '/core/Command/Db/Migrations/StatusCommand.php',
'OC\\Core\\Command\\Db\\SchemaEncoder' => $baseDir . '/core/Command/Db/SchemaEncoder.php',
'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => $baseDir . '/core/Command/Encryption/ChangeKeyStorageRoot.php',
Expand Down
17 changes: 17 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,22 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Mail\\IEMailTemplate' => __DIR__ . '/../../..' . '/lib/public/Mail/IEMailTemplate.php',
'OCP\\Mail\\IMailer' => __DIR__ . '/../../..' . '/lib/public/Mail/IMailer.php',
'OCP\\Mail\\IMessage' => __DIR__ . '/../../..' . '/lib/public/Mail/IMessage.php',
'OCP\\Migration\\Attributes\\AddColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/AddColumn.php',
'OCP\\Migration\\Attributes\\AddIndex' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/AddIndex.php',
'OCP\\Migration\\Attributes\\ColumnMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/ColumnMigrationAttribute.php',
'OCP\\Migration\\Attributes\\ColumnType' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/ColumnType.php',
'OCP\\Migration\\Attributes\\CreateTable' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/CreateTable.php',
'OCP\\Migration\\Attributes\\DeleteTable' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DeleteTable.php',
'OCP\\Migration\\Attributes\\DropColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropColumn.php',
'OCP\\Migration\\Attributes\\DropIndex' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropIndex.php',
'OCP\\Migration\\Attributes\\GenericMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/GenericMigrationAttribute.php',
'OCP\\Migration\\Attributes\\IndexMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/IndexMigrationAttribute.php',
'OCP\\Migration\\Attributes\\IndexType' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/IndexType.php',
'OCP\\Migration\\Attributes\\MigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/MigrationAttribute.php',
'OCP\\Migration\\Attributes\\ModifyColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/ModifyColumn.php',
'OCP\\Migration\\Attributes\\TableMigrationAttribute' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/TableMigrationAttribute.php',
'OCP\\Migration\\BigIntMigration' => __DIR__ . '/../../..' . '/lib/public/Migration/BigIntMigration.php',
'OCP\\Migration\\Exceptions\\AttributeException' => __DIR__ . '/../../..' . '/lib/public/Migration/Exceptions/AttributeException.php',
'OCP\\Migration\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php',
'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php',
'OCP\\Migration\\IRepairStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IRepairStep.php',
Expand Down Expand Up @@ -1151,7 +1166,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\Db\\ExportSchema' => __DIR__ . '/../../..' . '/core/Command/Db/ExportSchema.php',
'OC\\Core\\Command\\Db\\Migrations\\ExecuteCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/ExecuteCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\GenerateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\GenerateMetadataCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/GenerateMetadataCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\MigrateCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/MigrateCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\PreviewCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/PreviewCommand.php',
'OC\\Core\\Command\\Db\\Migrations\\StatusCommand' => __DIR__ . '/../../..' . '/core/Command/Db/Migrations/StatusCommand.php',
'OC\\Core\\Command\\Db\\SchemaEncoder' => __DIR__ . '/../../..' . '/core/Command/Db/SchemaEncoder.php',
'OC\\Core\\Command\\Encryption\\ChangeKeyStorageRoot' => __DIR__ . '/../../..' . '/core/Command/Encryption/ChangeKeyStorageRoot.php',
Expand Down
22 changes: 22 additions & 0 deletions lib/public/Migration/Attributes/AddColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Migration\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]

Check failure on line 13 in lib/public/Migration/Attributes/AddColumn.php

View workflow job for this annotation

GitHub Actions / static-code-analysis-ocp

InvalidDocblock

lib/public/Migration/Attributes/AddColumn.php:13:1: InvalidDocblock: PHPDoc is required for classes/interfaces in OCP. (see https://psalm.dev/008)
class AddColumn extends ColumnMigrationAttribute {
public function definition(): string {

Check failure on line 15 in lib/public/Migration/Attributes/AddColumn.php

View workflow job for this annotation

GitHub Actions / static-code-analysis-ocp

InvalidDocblock

lib/public/Migration/Attributes/AddColumn.php:15:2: InvalidDocblock: PHPDoc is required for methods in OCP. (see https://psalm.dev/008)
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')';
$table = empty($this->getTable()) ? '' : ' to table \'' . $this->getTable() . '\'';
return empty($this->getName()) ?
'Addition of a new column' . $type . $table
: 'Addition of column \'' . $this->getName() . '\'' . $type . $table;
}
}
20 changes: 20 additions & 0 deletions lib/public/Migration/Attributes/AddIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Migration\Attributes;

use Attribute;

#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]

Check failure on line 13 in lib/public/Migration/Attributes/AddIndex.php

View workflow job for this annotation

GitHub Actions / static-code-analysis-ocp

InvalidDocblock

lib/public/Migration/Attributes/AddIndex.php:13:1: InvalidDocblock: PHPDoc is required for classes/interfaces in OCP. (see https://psalm.dev/008)
class AddIndex extends IndexMigrationAttribute {
public function definition(): string {

Check failure on line 15 in lib/public/Migration/Attributes/AddIndex.php

View workflow job for this annotation

GitHub Actions / static-code-analysis-ocp

InvalidDocblock

lib/public/Migration/Attributes/AddIndex.php:15:2: InvalidDocblock: PHPDoc is required for methods in OCP. (see https://psalm.dev/008)
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')';
$table = empty($this->getTable()) ? '' : ' to table \'' . $this->getTable() . '\'';
return 'Addition of a new index' . $type . $table;
}
}
Loading

0 comments on commit c11cae7

Please sign in to comment.