aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMaxence Lange <maxence@artificial-owl.com>2024-07-17 10:49:11 -0100
committerMaxence Lange <maxence@artificial-owl.com>2024-07-29 12:44:52 -0100
commit88cfab4f329c7ee1f360be1ad7d90cf767da3720 (patch)
treead460429a5d58ef5ac67450cabc24e54bb9b1d9e
parent10821643645f1e9ab5d6d768bd52b37468722d3f (diff)
downloadnextcloud-server-88cfab4f329c7ee1f360be1ad7d90cf767da3720.tar.gz
nextcloud-server-88cfab4f329c7ee1f360be1ad7d90cf767da3720.zip
feat(upgrade): migration attributes
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>
-rw-r--r--core/Command/Db/Migrations/GenerateMetadataCommand.php106
-rw-r--r--core/Command/Db/Migrations/PreviewCommand.php162
-rw-r--r--core/register_command.php2
-rw-r--r--lib/composer/composer/autoload_classmap.php20
-rw-r--r--lib/composer/composer/autoload_static.php20
-rw-r--r--lib/public/Migration/Attributes/AddColumn.php22
-rw-r--r--lib/public/Migration/Attributes/AddIndex.php20
-rw-r--r--lib/public/Migration/Attributes/ColumnMigrationAttribute.php58
-rw-r--r--lib/public/Migration/Attributes/ColumnType.php46
-rw-r--r--lib/public/Migration/Attributes/CreateTable.php20
-rw-r--r--lib/public/Migration/Attributes/DeleteTable.php18
-rw-r--r--lib/public/Migration/Attributes/DropColumn.php21
-rw-r--r--lib/public/Migration/Attributes/DropIndex.php20
-rw-r--r--lib/public/Migration/Attributes/GenericMigrationAttribute.php31
-rw-r--r--lib/public/Migration/Attributes/IndexMigrationAttribute.php46
-rw-r--r--lib/public/Migration/Attributes/IndexType.php21
-rw-r--r--lib/public/Migration/Attributes/MigrationAttribute.php66
-rw-r--r--lib/public/Migration/Attributes/ModifyColumn.php22
-rw-r--r--lib/public/Migration/Attributes/TableMigrationAttribute.php46
-rw-r--r--lib/public/Migration/Exceptions/AttributeException.php17
20 files changed, 784 insertions, 0 deletions
diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php
new file mode 100644
index 00000000000..addcb59e68b
--- /dev/null
+++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php
@@ -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() {
+ $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;
+ }
+}
diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php
new file mode 100644
index 00000000000..17d1d8b01ec
--- /dev/null
+++ b/core/Command/Db/Migrations/PreviewCommand.php
@@ -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() {
+ $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] ?? []);
+ }
+
+ 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)
+ || !ctype_alpha(substr($class, strlen($namespace)))) {
+ throw new AttributeException('class name does not looks valid');
+ }
+
+ try {
+ $attribute = new $class();
+ return $attribute->import($item);
+ } catch (\Error) {
+ throw new AttributeException('cannot import Attribute');
+ }
+ }
+}
diff --git a/core/register_command.php b/core/register_command.php
index 6e89568cf9b..2922e98671a 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -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));
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index dc0c8f0e6c6..a167eba878f 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -572,6 +572,7 @@ return array(
'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',
+<<<<<<< HEAD
'OCP\\Mail\\Provider\\Address' => $baseDir . '/lib/public/Mail/Provider/Address.php',
'OCP\\Mail\\Provider\\Attachment' => $baseDir . '/lib/public/Mail/Provider/Attachment.php',
'OCP\\Mail\\Provider\\Exception\\Exception' => $baseDir . '/lib/public/Mail/Provider/Exception/Exception.php',
@@ -584,7 +585,24 @@ 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\\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',
+>>>>>>> 2f771df35a9 (feat(upgrade): migration attributes)
'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',
@@ -1143,7 +1161,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',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 635854db4c6..b328d6ff5c9 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -605,6 +605,7 @@ 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',
+<<<<<<< HEAD
'OCP\\Mail\\Provider\\Address' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Address.php',
'OCP\\Mail\\Provider\\Attachment' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Attachment.php',
'OCP\\Mail\\Provider\\Exception\\Exception' => __DIR__ . '/../../..' . '/lib/public/Mail/Provider/Exception/Exception.php',
@@ -617,7 +618,24 @@ 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\\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',
+>>>>>>> 2f771df35a9 (feat(upgrade): migration attributes)
'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',
@@ -1176,7 +1194,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',
diff --git a/lib/public/Migration/Attributes/AddColumn.php b/lib/public/Migration/Attributes/AddColumn.php
new file mode 100644
index 00000000000..aadf3263b31
--- /dev/null
+++ b/lib/public/Migration/Attributes/AddColumn.php
@@ -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)]
+class AddColumn extends ColumnMigrationAttribute {
+ public function definition(): string {
+ $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;
+ }
+}
diff --git a/lib/public/Migration/Attributes/AddIndex.php b/lib/public/Migration/Attributes/AddIndex.php
new file mode 100644
index 00000000000..9c96cd49a12
--- /dev/null
+++ b/lib/public/Migration/Attributes/AddIndex.php
@@ -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)]
+class AddIndex extends IndexMigrationAttribute {
+ public function definition(): string {
+ $type = is_null($this->getType()) ? '' : ' (' . $this->getType()->value . ')';
+ $table = empty($this->getTable()) ? '' : ' to table \'' . $this->getTable() . '\'';
+ return 'Addition of a new index' . $type . $table;
+ }
+}
diff --git a/lib/public/Migration/Attributes/ColumnMigrationAttribute.php b/lib/public/Migration/Attributes/ColumnMigrationAttribute.php
new file mode 100644
index 00000000000..6862f73e11d
--- /dev/null
+++ b/lib/public/Migration/Attributes/ColumnMigrationAttribute.php
@@ -0,0 +1,58 @@
+<?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;
+
+class ColumnMigrationAttribute extends MigrationAttribute implements JsonSerializable {
+ public function __construct(
+ string $table = '',
+ private string $name = '',
+ private ?ColumnType $type = null,
+ string $description = '',
+ array $notes = [],
+ ) {
+ parent::__construct($table, $description, $notes);
+ }
+
+ public function setName(string $name): self {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName(): string {
+ return $this->name;
+ }
+
+ public function setType(?ColumnType $type): self {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getType(): ?ColumnType {
+ return $this->type;
+ }
+
+ public function import(array $data): self {
+ parent::import($data);
+ $this->setName($data['name'] ?? '');
+ $this->setType(ColumnType::tryFrom($data['type'] ?? ''));
+ return $this;
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'name' => $this->getName(),
+ 'type' => $this->getType() ?? '',
+ ]
+ );
+ }
+}
diff --git a/lib/public/Migration/Attributes/ColumnType.php b/lib/public/Migration/Attributes/ColumnType.php
new file mode 100644
index 00000000000..058b74f6516
--- /dev/null
+++ b/lib/public/Migration/Attributes/ColumnType.php
@@ -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 FieldType 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';
+}
diff --git a/lib/public/Migration/Attributes/CreateTable.php b/lib/public/Migration/Attributes/CreateTable.php
new file mode 100644
index 00000000000..600d9bf4b9f
--- /dev/null
+++ b/lib/public/Migration/Attributes/CreateTable.php
@@ -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)]
+class CreateTable extends TableMigrationAttribute {
+ public function definition(): string {
+ $definition = empty($this->getTable()) ? 'Creation of a new table' : 'Creation of new table \'' . $this->getTable() . '\'';
+ $definition .= empty($this->getColumns()) ? '' : ' with columns ' . implode(', ', $this->getColumns());
+ return $definition;
+ }
+}
diff --git a/lib/public/Migration/Attributes/DeleteTable.php b/lib/public/Migration/Attributes/DeleteTable.php
new file mode 100644
index 00000000000..97bd36e385e
--- /dev/null
+++ b/lib/public/Migration/Attributes/DeleteTable.php
@@ -0,0 +1,18 @@
+<?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)]
+class DeleteTable extends MigrationAttribute {
+ public function definition(): string {
+ return empty($this->getTable()) ? 'Deletion of a table' : 'Deletion of table \'' . $this->getTable() . '\'';
+ }
+}
diff --git a/lib/public/Migration/Attributes/DropColumn.php b/lib/public/Migration/Attributes/DropColumn.php
new file mode 100644
index 00000000000..dfa1e81b315
--- /dev/null
+++ b/lib/public/Migration/Attributes/DropColumn.php
@@ -0,0 +1,21 @@
+<?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)]
+class DropColumn extends ColumnMigrationAttribute {
+ public function definition(): string {
+ $table = empty($this->getTable()) ? '' : ' from table \'' . $this->getTable() . '\'';
+ return empty($this->getName()) ?
+ 'Deletion of a column' . $table
+ : 'Deletion of column \'' . $this->getName() . '\'' . $table;
+ }
+}
diff --git a/lib/public/Migration/Attributes/DropIndex.php b/lib/public/Migration/Attributes/DropIndex.php
new file mode 100644
index 00000000000..cbeea4f5997
--- /dev/null
+++ b/lib/public/Migration/Attributes/DropIndex.php
@@ -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)]
+class DropIndex extends IndexMigrationAttribute {
+ public function definition(): string {
+ return empty($this->getTable()) ?
+ 'Deletion of an index'
+ : 'Deletion of an index from table \'' . $this->getTable() . '\'';
+ }
+}
diff --git a/lib/public/Migration/Attributes/GenericMigrationAttribute.php b/lib/public/Migration/Attributes/GenericMigrationAttribute.php
new file mode 100644
index 00000000000..f0e3af97615
--- /dev/null
+++ b/lib/public/Migration/Attributes/GenericMigrationAttribute.php
@@ -0,0 +1,31 @@
+<?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;
+
+class GenericMigrationAttribute extends MigrationAttribute implements JsonSerializable {
+ public function __construct(
+ private readonly array $details = []
+ ) {
+ parent::__construct(
+ $details['table'] ?? '',
+ $details['description'] ?? '',
+ $details['notes'] ?? []
+ );
+ }
+
+ public function definition(): string {
+ return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES);
+ }
+
+ public function jsonSerialize(): array {
+ return $this->details;
+ }
+}
diff --git a/lib/public/Migration/Attributes/IndexMigrationAttribute.php b/lib/public/Migration/Attributes/IndexMigrationAttribute.php
new file mode 100644
index 00000000000..fbe636bd6b0
--- /dev/null
+++ b/lib/public/Migration/Attributes/IndexMigrationAttribute.php
@@ -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;
+
+use JsonSerializable;
+
+class IndexMigrationAttribute extends MigrationAttribute implements JsonSerializable {
+ public function __construct(
+ string $table = '',
+ private ?IndexType $type = null,
+ string $description = '',
+ array $notes = [],
+ ) {
+ parent::__construct($table, $description, $notes);
+ }
+
+ public function setType(?IndexType $type): self {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function getType(): ?IndexType {
+ return $this->type;
+ }
+
+ public function import(array $data): self {
+ parent::import($data);
+ $this->setType(IndexType::tryFrom($data['type'] ?? ''));
+ return $this;
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'type' => $this->getType() ?? '',
+ ]
+ );
+ }
+}
diff --git a/lib/public/Migration/Attributes/IndexType.php b/lib/public/Migration/Attributes/IndexType.php
new file mode 100644
index 00000000000..842f135e700
--- /dev/null
+++ b/lib/public/Migration/Attributes/IndexType.php
@@ -0,0 +1,21 @@
+<?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;
+
+/**
+ * @since 30.0.0
+ */
+enum IndexType : string {
+ /** @since 30.0.0 */
+ case PRIMARY = 'primary'; // migration is estimated to require few minutes
+ /** @since 30.0.0 */
+ case INDEX = 'index'; // depends on setup, migration might require some time
+ /** @since 30.0.0 */
+ case UNIQUE = 'unique'; // migration should be light and quick
+}
diff --git a/lib/public/Migration/Attributes/MigrationAttribute.php b/lib/public/Migration/Attributes/MigrationAttribute.php
new file mode 100644
index 00000000000..9f88614e37d
--- /dev/null
+++ b/lib/public/Migration/Attributes/MigrationAttribute.php
@@ -0,0 +1,66 @@
+<?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;
+
+class MigrationAttribute implements JsonSerializable {
+ public function __construct(
+ private string $table = '',
+ private string $description = '',
+ private array $notes = [],
+ ) {
+ }
+
+ public function setTable(string $table): self {
+ $this->table = $table;
+ return $this;
+ }
+
+ public function getTable(): string {
+ return $this->table;
+ }
+
+ public function setDescription(string $description): self {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription(): string {
+ return $this->description;
+ }
+
+ public function setNotes(array $notes): self {
+ $this->notes = $notes;
+ return $this;
+ }
+
+ public function getNotes(): array {
+ return $this->notes;
+ }
+
+ public function definition(): string {
+ return json_encode($this->jsonSerialize(), JSON_UNESCAPED_SLASHES);
+ }
+
+ public function import(array $data): self {
+ return $this->setTable($data['table'] ?? '')
+ ->setDescription($data['description'] ?? '')
+ ->setNotes($data['notes'] ?? []);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'class' => get_class($this),
+ 'table' => $this->getTable(),
+ 'description' => $this->getDescription(),
+ 'notes' => $this->getNotes(),
+ ];
+ }
+}
diff --git a/lib/public/Migration/Attributes/ModifyColumn.php b/lib/public/Migration/Attributes/ModifyColumn.php
new file mode 100644
index 00000000000..e73b10478f7
--- /dev/null
+++ b/lib/public/Migration/Attributes/ModifyColumn.php
@@ -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)]
+class ModifyColumn extends ColumnMigrationAttribute {
+ public function definition(): string {
+ $type = is_null($this->getType()) ? '' : ' to ' . $this->getType()->value;
+ $table = empty($this->getTable()) ? '' : ' from table \'' . $this->getTable() . '\'';
+ return empty($this->getName()) ?
+ 'Modification of a column' . $table . $type
+ : 'Modification of column \'' . $this->getName() . '\'' . $table . $type;
+ }
+}
diff --git a/lib/public/Migration/Attributes/TableMigrationAttribute.php b/lib/public/Migration/Attributes/TableMigrationAttribute.php
new file mode 100644
index 00000000000..1c3f90c6acf
--- /dev/null
+++ b/lib/public/Migration/Attributes/TableMigrationAttribute.php
@@ -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;
+
+use JsonSerializable;
+
+class TableMigrationAttribute extends MigrationAttribute implements JsonSerializable {
+ public function __construct(
+ string $table = '',
+ private array $columns = [],
+ string $description = '',
+ array $notes = [],
+ ) {
+ parent::__construct($table, $description, $notes);
+ }
+
+ public function setColumns(array $columns): self {
+ $this->columns = $columns;
+ return $this;
+ }
+
+ public function getColumns(): array {
+ return $this->columns;
+ }
+
+ public function import(array $data): self {
+ parent::import($data);
+ $this->setColumns($data['columns'] ?? []);
+ return $this;
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'columns' => $this->getColumns(),
+ ]
+ );
+ }
+}
diff --git a/lib/public/Migration/Exceptions/AttributeException.php b/lib/public/Migration/Exceptions/AttributeException.php
new file mode 100644
index 00000000000..92de70f3e96
--- /dev/null
+++ b/lib/public/Migration/Exceptions/AttributeException.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 OCP\Migration\Exceptions;
+
+use Exception;
+
+/**
+ * @since 30.0.0
+ */
+class AttributeException extends Exception {
+}