Merge pull request #46476 from nextcloud/enh/noid/migration-attributes

Migration Attributes
pull/46885/head
Stephan Orbaugh 2024-07-30 15:54:56 +07:00 committed by GitHub
commit 18c0bcb2da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1618 additions and 4 deletions

@ -16,6 +16,7 @@ return array(
'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => $baseDir . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => $baseDir . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Locking\\FakeDBLockingProvider' => $baseDir . '/../lib/Locking/FakeDBLockingProvider.php',
'OCA\\Testing\\Migration\\Version30000Date20240102030405' => $baseDir . '/../lib/Migration/Version30000Date20240102030405.php',
'OCA\\Testing\\Provider\\FakeText2ImageProvider' => $baseDir . '/../lib/Provider/FakeText2ImageProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => $baseDir . '/../lib/Provider/FakeTextProcessingProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => $baseDir . '/../lib/Provider/FakeTextProcessingProviderSync.php',

@ -31,6 +31,7 @@ class ComposerStaticInitTesting
'OCA\\Testing\\Listener\\RegisterDeclarativeSettingsListener' => __DIR__ . '/..' . '/../lib/Listener/RegisterDeclarativeSettingsListener.php',
'OCA\\Testing\\Listener\\SetDeclarativeSettingsValueListener' => __DIR__ . '/..' . '/../lib/Listener/SetDeclarativeSettingsValueListener.php',
'OCA\\Testing\\Locking\\FakeDBLockingProvider' => __DIR__ . '/..' . '/../lib/Locking/FakeDBLockingProvider.php',
'OCA\\Testing\\Migration\\Version30000Date20240102030405' => __DIR__ . '/..' . '/../lib/Migration/Version30000Date20240102030405.php',
'OCA\\Testing\\Provider\\FakeText2ImageProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeText2ImageProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProvider.php',
'OCA\\Testing\\Provider\\FakeTextProcessingProviderSync' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProviderSync.php',

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Testing\Migration;
use Closure;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\AddIndex;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\Attributes\CreateTable;
use OCP\Migration\Attributes\DropColumn;
use OCP\Migration\Attributes\DropIndex;
use OCP\Migration\Attributes\DropTable;
use OCP\Migration\Attributes\IndexType;
use OCP\Migration\Attributes\ModifyColumn;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
#[DropTable(table: 'old_table')]
#[CreateTable(table: 'new_table', description: 'Table is used to store things, but also to get more things', notes: ['this is a notice', 'and another one, if really needed'])]
#[AddColumn(table: 'my_table')]
#[AddColumn(table: 'my_table', name: 'another_field')]
#[AddColumn(table: 'other_table', name: 'last_one', type: ColumnType::DATE)]
#[AddIndex(table: 'my_table')]
#[AddIndex(table: 'my_table', type: IndexType::PRIMARY)]
#[DropColumn(table: 'other_table')]
#[DropColumn(table: 'other_table', name: 'old_column', description: 'field is not used anymore and replaced by \'last_one\'')]
#[DropIndex(table: 'other_table')]
#[ModifyColumn(table: 'other_table')]
#[ModifyColumn(table: 'other_table', name: 'this_field')]
#[ModifyColumn(table: 'other_table', name: 'this_field', type: ColumnType::BIGINT)]
class Version30000Date20240102030405 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
return null;
}
}

@ -0,0 +1,83 @@
<?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 = \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->metadataManager->extractMigrationAttributes($appId);
if (!$alreadyLoaded) {
$this->appManager->disableApp($appId);
}
}
return $metadata;
}
}

@ -0,0 +1,102 @@
<?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();
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 = [];
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)]);
}
}
}

@ -67,6 +67,8 @@ if ($config->getSystemValueBool('installed', false)) {
$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));

@ -584,6 +584,20 @@ return array(
'OCP\\Mail\\Provider\\IProvider' => $baseDir . '/lib/public/Mail/Provider/IProvider.php',
'OCP\\Mail\\Provider\\IService' => $baseDir . '/lib/public/Mail/Provider/IService.php',
'OCP\\Mail\\Provider\\Message' => $baseDir . '/lib/public/Mail/Provider/Message.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\\DropColumn' => $baseDir . '/lib/public/Migration/Attributes/DropColumn.php',
'OCP\\Migration\\Attributes\\DropIndex' => $baseDir . '/lib/public/Migration/Attributes/DropIndex.php',
'OCP\\Migration\\Attributes\\DropTable' => $baseDir . '/lib/public/Migration/Attributes/DropTable.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\\IMigrationStep' => $baseDir . '/lib/public/Migration/IMigrationStep.php',
'OCP\\Migration\\IOutput' => $baseDir . '/lib/public/Migration/IOutput.php',
@ -1143,7 +1157,9 @@ return array(
'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',
@ -1652,6 +1668,8 @@ return array(
'OC\\MemoryInfo' => $baseDir . '/lib/private/MemoryInfo.php',
'OC\\Migration\\BackgroundRepair' => $baseDir . '/lib/private/Migration/BackgroundRepair.php',
'OC\\Migration\\ConsoleOutput' => $baseDir . '/lib/private/Migration/ConsoleOutput.php',
'OC\\Migration\\Exceptions\\AttributeException' => $baseDir . '/lib/private/Migration/Exceptions/AttributeException.php',
'OC\\Migration\\MetadataManager' => $baseDir . '/lib/private/Migration/MetadataManager.php',
'OC\\Migration\\NullOutput' => $baseDir . '/lib/private/Migration/NullOutput.php',
'OC\\Migration\\SimpleOutput' => $baseDir . '/lib/private/Migration/SimpleOutput.php',
'OC\\NaturalSort' => $baseDir . '/lib/private/NaturalSort.php',
@ -1941,6 +1959,8 @@ return array(
'OC\\Updater\\Changes' => $baseDir . '/lib/private/Updater/Changes.php',
'OC\\Updater\\ChangesCheck' => $baseDir . '/lib/private/Updater/ChangesCheck.php',
'OC\\Updater\\ChangesMapper' => $baseDir . '/lib/private/Updater/ChangesMapper.php',
'OC\\Updater\\Exceptions\\ReleaseMetadataException' => $baseDir . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php',
'OC\\Updater\\ReleaseMetadata' => $baseDir . '/lib/private/Updater/ReleaseMetadata.php',
'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php',
'OC\\UserStatus\\ISettableProvider' => $baseDir . '/lib/private/UserStatus/ISettableProvider.php',
'OC\\UserStatus\\Manager' => $baseDir . '/lib/private/UserStatus/Manager.php',

@ -617,6 +617,20 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Mail\\Provider\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IProvider.php',
'OCP\\Mail\\Provider\\IService' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/IService.php',
'OCP\\Mail\\Provider\\Message' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Message.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\\DropColumn' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropColumn.php',
'OCP\\Migration\\Attributes\\DropIndex' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropIndex.php',
'OCP\\Migration\\Attributes\\DropTable' => __DIR__ . '/../../..' . '/lib/public/Migration/Attributes/DropTable.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\\IMigrationStep' => __DIR__ . '/../../..' . '/lib/public/Migration/IMigrationStep.php',
'OCP\\Migration\\IOutput' => __DIR__ . '/../../..' . '/lib/public/Migration/IOutput.php',
@ -1176,7 +1190,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',
@ -1685,6 +1701,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\MemoryInfo' => __DIR__ . '/../../..' . '/lib/private/MemoryInfo.php',
'OC\\Migration\\BackgroundRepair' => __DIR__ . '/../../..' . '/lib/private/Migration/BackgroundRepair.php',
'OC\\Migration\\ConsoleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/ConsoleOutput.php',
'OC\\Migration\\Exceptions\\AttributeException' => __DIR__ . '/../../..' . '/lib/private/Migration/Exceptions/AttributeException.php',
'OC\\Migration\\MetadataManager' => __DIR__ . '/../../..' . '/lib/private/Migration/MetadataManager.php',
'OC\\Migration\\NullOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/NullOutput.php',
'OC\\Migration\\SimpleOutput' => __DIR__ . '/../../..' . '/lib/private/Migration/SimpleOutput.php',
'OC\\NaturalSort' => __DIR__ . '/../../..' . '/lib/private/NaturalSort.php',
@ -1974,6 +1992,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Updater\\Changes' => __DIR__ . '/../../..' . '/lib/private/Updater/Changes.php',
'OC\\Updater\\ChangesCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesCheck.php',
'OC\\Updater\\ChangesMapper' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesMapper.php',
'OC\\Updater\\Exceptions\\ReleaseMetadataException' => __DIR__ . '/../../..' . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php',
'OC\\Updater\\ReleaseMetadata' => __DIR__ . '/../../..' . '/lib/private/Updater/ReleaseMetadata.php',
'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php',
'OC\\UserStatus\\ISettableProvider' => __DIR__ . '/../../..' . '/lib/private/UserStatus/ISettableProvider.php',
'OC\\UserStatus\\Manager' => __DIR__ . '/../../..' . '/lib/private/UserStatus/Manager.php',

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Migration\Exceptions;
use Exception;
/**
* @since 30.0.0
*/
class AttributeException extends Exception {
}

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Migration;
use OC\DB\Connection;
use OC\DB\MigrationService;
use OC\Migration\Exceptions\AttributeException;
use OCP\App\IAppManager;
use OCP\Migration\Attributes\GenericMigrationAttribute;
use OCP\Migration\Attributes\MigrationAttribute;
use Psr\Log\LoggerInterface;
use ReflectionClass;
/**
* Helps managing DB Migrations' Metadata
*
* @since 30.0.0
*/
class MetadataManager {
public function __construct(
private readonly IAppManager $appManager,
private readonly Connection $connection,
private readonly LoggerInterface $logger,
) {
}
/**
* We get all migrations from an app (or 'core'), and
* for each migration files we extract its attributes
*
* @param string $appId
*
* @return array<string, MigrationAttribute[]>
* @since 30.0.0
*/
public 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) {
$item = $attribute->newInstance();
if ($item instanceof MigrationAttribute) {
$metadata[$version][] = $item;
}
}
}
return $metadata;
}
/**
* convert direct data from release metadata into a list of Migrations' Attribute
*
* @param array<array-key, array<array-key, array>> $metadata
* @param bool $filterKnownMigrations ignore metadata already done in local instance
*
* @return array{apps: array<array-key, array<string, MigrationAttribute[]>>, core: array<string, MigrationAttribute[]>}
* @since 30.0.0
*/
public function getMigrationsAttributesFromReleaseMetadata(
array $metadata,
bool $filterKnownMigrations = false
): array {
$appsAttributes = [];
foreach (array_keys($metadata['apps']) as $appId) {
if ($filterKnownMigrations && !$this->appManager->isInstalled($appId)) {
continue; // if not interested and app is not installed
}
$done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : [];
$appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done);
}
$done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : [];
return [
'core' => $this->parseMigrations($metadata['core'] ?? [], $done),
'apps' => $appsAttributes
];
}
/**
* convert raw data to a list of MigrationAttribute
*
* @param array $migrations
* @param array $ignoreMigrations
*
* @return array<string, MigrationAttribute[]>
*/
private function parseMigrations(array $migrations, array $ignoreMigrations = []): array {
$parsed = [];
foreach (array_keys($migrations) as $entry) {
if (in_array($entry, $ignoreMigrations)) {
continue;
}
$parsed[$entry] = [];
foreach ($migrations[$entry] 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;
}
/**
* returns migrations already done
*
* @param string $appId
*
* @return array
* @throws \Exception
*/
private function getKnownMigrations(string $appId): array {
$ms = new MigrationService($appId, $this->connection);
return $ms->getMigratedVersions();
}
/**
* generate (deserialize) a MigrationAttribute from a serialized version
*
* @param array $item
*
* @return MigrationAttribute
* @throws AttributeException
*/
private function createAttribute(array $item): MigrationAttribute {
$class = $item['class'] ?? '';
$namespace = 'OCP\Migration\Attributes\\';
if (!str_starts_with($class, $namespace)
|| !ctype_alpha(substr($class, strlen($namespace)))) {
throw new AttributeException('class name does not looks valid');
}
try {
$attribute = new $class($item['table'] ?? '');
return $attribute->import($item);
} catch (\Error) {
throw new AttributeException('cannot import Attribute');
}
}
}

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Updater\Exceptions;
use Exception;
/**
* @since 30.0.0
*/
class ReleaseMetadataException extends Exception {
}

@ -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\Updater;
use Exception;
use JsonException;
use OC\Updater\Exceptions\ReleaseMetadataException;
use OCP\Http\Client\IClientService;
/** retrieve releases metadata from official servers
*
* @since 30.0.0
*/
class ReleaseMetadata {
public function __construct(
private readonly IClientService $clientService,
) {
}
/**
* returns metadata based on release version
*
* - version is a stable release, metadata is downloaded from official releases folder
* - version is not a table release, metadata is downloaded from official prereleases folder
* - version is a major version (30, 31, 32, ...), latest metadata are downloaded
*
* @param string $version
*
* @return array
* @throws ReleaseMetadataException
* @since 30.0.0
*/
public function getMetadata(string $version): array {
if (!str_contains($version, '.')) {
$url = 'https://download.nextcloud.com/server/releases/latest-' . $version . '.metadata';
} else {
[,,$minor] = explode('.', $version);
if (ctype_digit($minor)) {
$url = 'https://download.nextcloud.com/server/releases/nextcloud-' . $version . '.metadata';
} else {
$url = 'https://download.nextcloud.com/server/prereleases/nextcloud-' . $version . '.metadata';
}
}
return $this->downloadMetadata($url);
}
/**
* download Metadata from a link
*
* @param string $url
*
* @return array
* @throws ReleaseMetadataException
* @since 30.0.0
*/
public function downloadMetadata(string $url): array {
$client = $this->clientService->newClient();
try {
$response = $client->get($url, [
'timeout' => 10,
'connect_timeout' => 10
]);
} catch (Exception $e) {
throw new ReleaseMetadataException('could not reach metadata at ' . $url, previous: $e);
}
try {
return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
throw new ReleaseMetadataException('remote document is not valid');
}
}
}

@ -0,0 +1,30 @@
<?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 on new column creation
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class AddColumn extends ColumnMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')';
return empty($this->getName()) ?
'Addition of a new column' . $type . ' to table \'' . $this->getTable() . '\''
: 'Addition of column \'' . $this->getName() . '\'' . $type . ' to table \'' . $this->getTable() . '\'';
}
}

@ -0,0 +1,28 @@
<?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 on index creation
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class AddIndex extends IndexMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
$type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')';
return 'Addition of a new index' . $type . ' to table \'' . $this->getTable() . '\'';
}
}

@ -0,0 +1,101 @@
<?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 JsonSerializable;
/**
* generic class related to migration attribute about column changes
*
* @since 30.0.0
*/
class ColumnMigrationAttribute extends MigrationAttribute implements JsonSerializable {
/**
* @param string $table name of the database table
* @param string $name name of the column
* @param ColumnType|null $type type of the column
* @param string $description description of the migration
* @param array $notes notes about the migration/column
* @since 30.0.0
*/
public function __construct(
string $table,
private string $name = '',
private ?ColumnType $type = null,
string $description = '',
array $notes = [],
) {
parent::__construct($table, $description, $notes);
}
/**
* @param string $name
*
* @return $this
* @since 30.0.0
*/
public function setName(string $name): self {
$this->name = $name;
return $this;
}
/**
* @return string
* @since 30.0.0
*/
public function getName(): string {
return $this->name;
}
/**
* @param ColumnType|null $type
*
* @return $this
* @since 30.0.0
*/
public function setType(?ColumnType $type): self {
$this->type = $type;
return $this;
}
/**
* @return ColumnType|null
* @since 30.0.0
*/
public function getType(): ?ColumnType {
return $this->type;
}
/**
* @param array $data
*
* @return $this
* @since 30.0.0
*/
public function import(array $data): self {
parent::import($data);
$this->setName($data['name'] ?? '');
$this->setType(ColumnType::tryFrom($data['type'] ?? ''));
return $this;
}
/**
* @return array
* @since 30.0.0
*/
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'name' => $this->getName(),
'type' => $this->getType() ?? '',
]
);
}
}

@ -0,0 +1,46 @@
<?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;
/**
* enum ColumnType based on OCP\DB\Types
*
* @see \OCP\DB\Types
* @since 30.0.0
*/
enum ColumnType : string {
/** @since 30.0.0 */
case BIGINT = 'bigint';
/** @since 30.0.0 */
case BINARY = 'binary';
/** @since 30.0.0 */
case BLOB = 'blob';
/** @since 30.0.0 */
case BOOLEAN = 'boolean';
/** @since 30.0.0 */
case DATE = 'date';
/** @since 30.0.0 */
case DATETIME = 'datetime';
/** @since 30.0.0 */
case DECIMAL = 'decimal';
/** @since 30.0.0 */
case FLOAT = 'float';
/** @since 30.0.0 */
case INTEGER = 'integer';
/** @since 30.0.0 */
case SMALLINT = 'smallint';
/** @since 30.0.0 */
case STRING = 'string';
/** @since 30.0.0 */
case TEXT = 'text';
/** @since 30.0.0 */
case TIME = 'time';
/** @since 30.0.0 */
case JSON = 'json';
}

@ -0,0 +1,29 @@
<?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 on table creation
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class CreateTable extends TableMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
$definition = 'Creation of new table \'' . $this->getTable() . '\'';
$definition .= empty($this->getColumns()) ? '' : ' with columns ' . implode(', ', $this->getColumns());
return $definition;
}
}

@ -0,0 +1,29 @@
<?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 on column drop
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class DropColumn extends ColumnMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
return empty($this->getName()) ?
'Deletion of a column from table \'' . $this->getTable() . '\''
: 'Deletion of column \'' . $this->getName() . '\' from table \'' . $this->getTable() . '\'';
}
}

@ -0,0 +1,27 @@
<?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 on index drop
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class DropIndex extends IndexMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
return 'Deletion of an index from table \'' . $this->getTable() . '\'';
}
}

@ -0,0 +1,27 @@
<?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 on table drop
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class DropTable extends TableMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
return 'Deletion of table \'' . $this->getTable() . '\'';
}
}

@ -0,0 +1,49 @@
<?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 JsonSerializable;
/**
* generic entry, used to replace migration attribute not yet known in current version
* but used in a future release
*
* @since 30.0.0
*/
class GenericMigrationAttribute extends MigrationAttribute implements JsonSerializable {
/**
* @param array $details
* @since 30.0.0
*/
public function __construct(
private readonly array $details = []
) {
parent::__construct(
$details['table'] ?? '',
$details['description'] ?? '',
$details['notes'] ?? []
);
}
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES);
}
/**
* @return array
* @since 30.0.0
*/
public function jsonSerialize(): array {
return $this->details;
}
}

@ -0,0 +1,78 @@
<?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 JsonSerializable;
/**
* generic class related to migration attribute about index changes
*
* @since 30.0.0
*/
class IndexMigrationAttribute extends MigrationAttribute implements JsonSerializable {
/**
* @param string $table name of the database table
* @param IndexType|null $type type of the index
* @param string $description description of the migration
* @param array $notes notes abour the migration/index
* @since 30.0.0
*/
public function __construct(
string $table,
private ?IndexType $type = null,
string $description = '',
array $notes = [],
) {
parent::__construct($table, $description, $notes);
}
/**
* @param IndexType|null $type
*
* @return $this
* @since 30.0.0
*/
public function setType(?IndexType $type): self {
$this->type = $type;
return $this;
}
/**
* @return IndexType|null
* @since 30.0.0
*/
public function getType(): ?IndexType {
return $this->type;
}
/**
* @param array $data
*
* @return $this
* @since 30.0.0
*/
public function import(array $data): self {
parent::import($data);
$this->setType(IndexType::tryFrom($data['type'] ?? ''));
return $this;
}
/**
* @return array
* @since 30.0.0
*/
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'type' => $this->getType() ?? '',
]
);
}
}

@ -0,0 +1,23 @@
<?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;
/**
* type of index
*
* @since 30.0.0
*/
enum IndexType : string {
/** @since 30.0.0 */
case PRIMARY = 'primary';
/** @since 30.0.0 */
case INDEX = 'index';
/** @since 30.0.0 */
case UNIQUE = 'unique';
}

@ -0,0 +1,118 @@
<?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 JsonSerializable;
/**
* @since 30.0.0
*/
class MigrationAttribute implements JsonSerializable {
/**
* @param string $table name of the database table
* @param string $description description of the migration
* @param array $notes notes about the migration
* @since 30.0.0
*/
public function __construct(
private string $table,
private string $description = '',
private array $notes = [],
) {
}
/**
* @param string $table
*
* @return $this
* @since 30.0.0
*/
public function setTable(string $table): self {
$this->table = $table;
return $this;
}
/**
* @return string
* @since 30.0.0
*/
public function getTable(): string {
return $this->table;
}
/**
* @param string $description
*
* @return $this
* @since 30.0.0
*/
public function setDescription(string $description): self {
$this->description = $description;
return $this;
}
/**
* @return string
* @since 30.0.0
*/
public function getDescription(): string {
return $this->description;
}
/**
* @param array $notes
*
* @return $this
* @since 30.0.0
*/
public function setNotes(array $notes): self {
$this->notes = $notes;
return $this;
}
/**
* @return array
* @since 30.0.0
*/
public function getNotes(): array {
return $this->notes;
}
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES);
}
/**
* @param array $data
*
* @return self
* @since 30.0.0
*/
public function import(array $data): self {
return $this->setDescription($data['description'] ?? '')
->setNotes($data['notes'] ?? []);
}
/**
* @return array
* @since 30.0.0
*/
public function jsonSerialize(): array {
return [
'class' => get_class($this),
'table' => $this->getTable(),
'description' => $this->getDescription(),
'notes' => $this->getNotes()
];
}
}

@ -0,0 +1,30 @@
<?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 on column modification
*
* @since 30.0.0
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class ModifyColumn extends ColumnMigrationAttribute {
/**
* @return string
* @since 30.0.0
*/
public function definition(): string {
$type = is_null($this->getType()) ? '' : ' to ' . $this->getType()->value;
return empty($this->getName()) ?
'Modification of a column from table \'' . $this->getTable() . '\'' . $type
: 'Modification of column \'' . $this->getName() . '\' from table \'' . $this->getTable() . '\'' . $type;
}
}

@ -0,0 +1,78 @@
<?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 JsonSerializable;
/**
* generic class related to migration attribute about table changes
*
* @since 30.0.0
*/
class TableMigrationAttribute extends MigrationAttribute implements JsonSerializable {
/**
* @param string $table name of the database table
* @param array $columns list of columns
* @param string $description description of the migration
* @param array $notes notes about the migration/table
* @since 30.0.0
*/
public function __construct(
string $table,
private array $columns = [],
string $description = '',
array $notes = [],
) {
parent::__construct($table, $description, $notes);
}
/**
* @param array $columns
*
* @return $this
* @since 30.0.0
*/
public function setColumns(array $columns): self {
$this->columns = $columns;
return $this;
}
/**
* @return array
* @since 30.0.0
*/
public function getColumns(): array {
return $this->columns;
}
/**
* @param array $data
*
* @return $this
* @since 30.0.0
*/
public function import(array $data): self {
parent::import($data);
$this->setColumns($data['columns'] ?? []);
return $this;
}
/**
* @return array
* @since 30.0.0
*/
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'columns' => $this->getColumns(),
]
);
}
}

@ -19,8 +19,21 @@ use Doctrine\DBAL\Types\Type;
use OC\DB\Connection;
use OC\DB\MigrationService;
use OC\DB\SchemaWrapper;
use OC\Migration\MetadataManager;
use OCP\App\IAppManager;
use OCP\IDBConnection;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\AddIndex;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\Attributes\CreateTable;
use OCP\Migration\Attributes\DropColumn;
use OCP\Migration\Attributes\DropIndex;
use OCP\Migration\Attributes\DropTable;
use OCP\Migration\Attributes\IndexType;
use OCP\Migration\Attributes\ModifyColumn;
use OCP\Migration\IMigrationStep;
use OCP\Server;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Class MigrationsTest
@ -28,10 +41,9 @@ use OCP\Migration\IMigrationStep;
* @package Test\DB
*/
class MigrationsTest extends \Test\TestCase {
/** @var MigrationService | \PHPUnit\Framework\MockObject\MockObject */
private $migrationService;
/** @var \PHPUnit\Framework\MockObject\MockObject | IDBConnection $db */
private $db;
private MigrationService|MockObject $migrationService;
private MockObject|IDBConnection $db;
private IAppManager $appManager;
protected function setUp(): void {
parent::setUp();
@ -39,6 +51,8 @@ class MigrationsTest extends \Test\TestCase {
$this->db = $this->createMock(Connection::class);
$this->db->expects($this->any())->method('getPrefix')->willReturn('test_oc_');
$this->migrationService = new MigrationService('testing', $this->db);
$this->appManager = Server::get(IAppManager::class);
}
public function testGetters() {
@ -755,4 +769,163 @@ class MigrationsTest extends \Test\TestCase {
self::invokePrivate($this->migrationService, 'ensureOracleConstraints', [$sourceSchema, $schema, 3]);
}
public function testExtractMigrationAttributes() {
$metadataManager = Server::get(MetadataManager::class);
$this->appManager->loadApp('testing');
$this->assertEquals($this->getMigrationMetadata(), json_decode(json_encode($metadataManager->extractMigrationAttributes('testing')), true));
$this->appManager->disableApp('testing');
}
public function testDeserializeMigrationMetadata() {
$metadataManager = Server::get(MetadataManager::class);
$this->assertEquals(
[
'core' => [],
'apps' => [
'testing' => [
'30000Date20240102030405' => [
new DropTable('old_table'),
new CreateTable('new_table',
description: 'Table is used to store things, but also to get more things',
notes: ['this is a notice', 'and another one, if really needed']
),
new AddColumn('my_table'),
new AddColumn('my_table', 'another_field'),
new AddColumn('other_table', 'last_one', ColumnType::DATE),
new AddIndex('my_table'),
new AddIndex('my_table', IndexType::PRIMARY),
new DropColumn('other_table'),
new DropColumn('other_table', 'old_column',
description: 'field is not used anymore and replaced by \'last_one\''
),
new DropIndex('other_table'),
new ModifyColumn('other_table'),
new ModifyColumn('other_table', 'this_field'),
new ModifyColumn('other_table', 'this_field', ColumnType::BIGINT)
]
]
]
],
$metadataManager->getMigrationsAttributesFromReleaseMetadata(
[
'core' => [],
'apps' => ['testing' => $this->getMigrationMetadata()]
]
)
);
}
private function getMigrationMetadata(): array {
return [
'30000Date20240102030405' => [
[
'class' => 'OCP\\Migration\\Attributes\\DropTable',
'table' => 'old_table',
'description' => '',
'notes' => [],
'columns' => []
],
[
'class' => 'OCP\\Migration\\Attributes\\CreateTable',
'table' => 'new_table',
'description' => 'Table is used to store things, but also to get more things',
'notes' =>
[
'this is a notice',
'and another one, if really needed'
],
'columns' => []
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'my_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'my_table',
'description' => '',
'notes' => [],
'name' => 'another_field',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'last_one',
'type' => 'date'
],
[
'class' => 'OCP\\Migration\\Attributes\\AddIndex',
'table' => 'my_table',
'description' => '',
'notes' => [],
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddIndex',
'table' => 'my_table',
'description' => '',
'notes' => [],
'type' => 'primary'
],
[
'class' => 'OCP\\Migration\\Attributes\\DropColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\DropColumn',
'table' => 'other_table',
'description' => 'field is not used anymore and replaced by \'last_one\'',
'notes' => [],
'name' => 'old_column',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\DropIndex',
'table' => 'other_table',
'description' => '',
'notes' => [],
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'this_field',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'this_field',
'type' => 'bigint'
],
]
];
}
}

@ -0,0 +1,209 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace Test\Updater;
use OC\Updater\ReleaseMetadata;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use PHPUnit\Framework\MockObject\MockObject;
class ReleaseMetadataTest extends \Test\TestCase {
private IClientService| MockObject $clientService;
protected function setUp(): void {
parent::setUp();
$this->clientService = $this->getMockBuilder(IClientService::class)
->disableOriginalConstructor()
->getMock();
}
public function testDownloadMetadata() {
$client = $this->createMock(IClient::class);
$response = $this->createMock(IResponse::class);
$this->clientService->expects($this->once())
->method('newClient')
->with()
->willReturn($client);
$client->expects($this->once())
->method('get')
->willReturn($response);
$response->expects($this->once())
->method('getBody')
->with()
->willReturn($this->resultRequest());
$releaseMetadata = new ReleaseMetadata($this->clientService);
$this->assertSame($this->resultRequestArray(), $releaseMetadata->downloadMetadata('ouila'));
}
/**
* @dataProvider getMetadataUrlProvider
*
* @param string $version
* @param string $url
*/
public function testGetMetadata(string $version, string $url) {
$client = $this->createMock(IClient::class);
$response = $this->createMock(IResponse::class);
$this->clientService->expects($this->once())
->method('newClient')
->with()
->willReturn($client);
$client->expects($this->once())
->method('get')
->with($url)
->willReturn($response);
$response->expects($this->once())
->method('getBody')
->with()
->willReturn('{}');
$releaseMetadata = new ReleaseMetadata($this->clientService);
$releaseMetadata->getMetadata($version);
}
/**
* @return array
*/
public function getMetadataUrlProvider(): array {
return [
[
'30.0.0',
'https://download.nextcloud.com/server/releases/nextcloud-30.0.0.metadata'
],
[
'30.0.0-beta1',
'https://download.nextcloud.com/server/prereleases/nextcloud-30.0.0-beta1.metadata'
],
[
'30',
'https://download.nextcloud.com/server/releases/latest-30.metadata'
]
];
}
private function resultRequest(): string {
return json_encode($this->resultRequestArray());
}
private function resultRequestArray(): array {
return [
'migrations' => [
'core' => [],
'apps' => [
'testing' => [
'30000Date20240102030405' => [
'class' => 'OCP\\Migration\\Attributes\\DropTable',
'table' => 'old_table',
'description' => '',
'notes' => [],
'columns' => []
],
[
'class' => 'OCP\\Migration\\Attributes\\CreateTable',
'table' => 'new_table',
'description' => 'Table is used to store things, but also to get more things',
'notes' => [
'this is a notice',
'and another one, if really needed'
],
'columns' => []
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'my_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'my_table',
'description' => '',
'notes' => [],
'name' => 'another_field',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'last_one',
'type' => 'date'
],
[
'class' => 'OCP\\Migration\\Attributes\\AddIndex',
'table' => 'my_table',
'description' => '',
'notes' => [],
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\AddIndex',
'table' => 'my_table',
'description' => '',
'notes' => [],
'type' => 'primary'
],
[
'class' => 'OCP\\Migration\\Attributes\\DropColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\DropColumn',
'table' => 'other_table',
'description' => 'field is not used anymore and replaced by \'last_one\'',
'notes' => [],
'name' => 'old_column',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\DropIndex',
'table' => 'other_table',
'description' => '',
'notes' => [],
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => '',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'this_field',
'type' => ''
],
[
'class' => 'OCP\\Migration\\Attributes\\ModifyColumn',
'table' => 'other_table',
'description' => '',
'notes' => [],
'name' => 'this_field',
'type' => 'bigint'
]
]
]
]
];
}
}