diff options
author | skjnldsv <skjnldsv@protonmail.com> | 2025-07-31 08:53:35 +0200 |
---|---|---|
committer | skjnldsv <skjnldsv@protonmail.com> | 2025-08-06 09:00:50 +0200 |
commit | 612e06466adb971d11f90396cf40fa1479b73954 (patch) | |
tree | 1efe564ae8b14e108b3fd4efe878414c787d875c | |
parent | c3cd5bb439dde62ba9e1a812b076ba3c2cd7897e (diff) | |
download | nextcloud-server-backport/54269/stable31.tar.gz nextcloud-server-backport/54269/stable31.zip |
fix(core): ensure unique vcategorybackport/54269/stable31
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r-- | core/Application.php | 6 | ||||
-rw-r--r-- | core/Migrations/Version13000Date20170718121200.php | 1 | ||||
-rw-r--r-- | core/Migrations/Version31000Date20250731062008.php | 106 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/Tags.php | 1 | ||||
-rw-r--r-- | version.php | 2 |
7 files changed, 116 insertions, 2 deletions
diff --git a/core/Application.php b/core/Application.php index 9a759da544c..cfca7b3f4ad 100644 --- a/core/Application.php +++ b/core/Application.php @@ -237,6 +237,12 @@ class Application extends App { 'systag_objecttype', ['objecttype'] ); + + $event->addMissingUniqueIndex( + 'vcategory', + 'unique_category_per_user', + ['uid', 'type', 'category'] + ); }); $eventDispatcher->addListener(AddMissingPrimaryKeyEvent::class, function (AddMissingPrimaryKeyEvent $event) { diff --git a/core/Migrations/Version13000Date20170718121200.php b/core/Migrations/Version13000Date20170718121200.php index 1adbf2f0ea2..4a0bc8b7eee 100644 --- a/core/Migrations/Version13000Date20170718121200.php +++ b/core/Migrations/Version13000Date20170718121200.php @@ -657,6 +657,7 @@ class Version13000Date20170718121200 extends SimpleMigrationStep { $table->addIndex(['uid'], 'uid_index'); $table->addIndex(['type'], 'type_index'); $table->addIndex(['category'], 'category_index'); + $table->addUniqueIndex(['uid', 'type', 'category'], 'unique_category_per_user'); } if (!$schema->hasTable('vcategory_to_object')) { diff --git a/core/Migrations/Version31000Date20250731062008.php b/core/Migrations/Version31000Date20250731062008.php new file mode 100644 index 00000000000..6730a64e896 --- /dev/null +++ b/core/Migrations/Version31000Date20250731062008.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Override; + +/** + * Make sure vcategory entries are unique per user and type + * This migration will clean up existing duplicates + * and add a unique constraint to prevent future duplicates. + */ +class Version31000Date20250731062008 extends SimpleMigrationStep { + public function __construct( + private IDBConnection $connection, + ) { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + #[Override] + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + // Clean up duplicate categories before adding unique constraint + $this->cleanupDuplicateCategories($output); + } + + /** + * Clean up duplicate categories + */ + private function cleanupDuplicateCategories(IOutput $output) { + $output->info('Starting cleanup of duplicate vcategory records...'); + + // Find all categories, ordered to identify duplicates + $qb = $this->connection->getQueryBuilder(); + $qb->select('id', 'uid', 'type', 'category') + ->from('vcategory') + ->orderBy('uid') + ->addOrderBy('type') + ->addOrderBy('category') + ->addOrderBy('id'); + + $result = $qb->executeQuery(); + + $seen = []; + $duplicateCount = 0; + + while ($category = $result->fetch()) { + $key = $category['uid'] . '|' . $category['type'] . '|' . $category['category']; + $categoryId = (int)$category['id']; + + if (!isset($seen[$key])) { + // First occurrence - keep this one + $seen[$key] = $categoryId; + continue; + } + + // Duplicate found + $keepId = $seen[$key]; + $duplicateCount++; + + $output->info("Found duplicate: keeping ID $keepId, removing ID $categoryId"); + + // Update object references + $updateQb = $this->connection->getQueryBuilder(); + $updateQb->update('vcategory_to_object') + ->set('categoryid', $updateQb->createNamedParameter($keepId)) + ->where($updateQb->expr()->eq('categoryid', $updateQb->createNamedParameter($categoryId))); + + $affectedRows = $updateQb->executeStatement(); + if ($affectedRows > 0) { + $output->info(" - Updated $affectedRows object references from category $categoryId to $keepId"); + } + + // Remove duplicate category record + $deleteQb = $this->connection->getQueryBuilder(); + $deleteQb->delete('vcategory') + ->where($deleteQb->expr()->eq('id', $deleteQb->createNamedParameter($categoryId))); + + $deleteQb->executeStatement(); + $output->info(" - Deleted duplicate category record ID $categoryId"); + + } + + $result->closeCursor(); + + if ($duplicateCount === 0) { + $output->info('No duplicate categories found'); + } else { + $output->info("Duplicate cleanup completed - processed $duplicateCount duplicates"); + } + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5b7821a7438..2c1872e23d9 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1461,6 +1461,7 @@ return array( 'OC\\Core\\Migrations\\Version31000Date20240101084401' => $baseDir . '/core/Migrations/Version31000Date20240101084401.php', 'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php', 'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php', + 'OC\\Core\\Migrations\\Version31000Date20250731062008' => $baseDir . '/core/Migrations/Version31000Date20250731062008.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1c22c0957f2..9e843c976d5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1510,6 +1510,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version31000Date20240101084401' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240101084401.php', 'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php', 'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php', + 'OC\\Core\\Migrations\\Version31000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250731062008.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', diff --git a/lib/private/Tags.php b/lib/private/Tags.php index 0a37f4c9f4e..fe4a4137e10 100644 --- a/lib/private/Tags.php +++ b/lib/private/Tags.php @@ -273,7 +273,6 @@ class Tags implements ITags { return false; } if ($this->userHasTag($name, $this->user)) { - // TODO use unique db properties instead of an additional check $this->logger->debug(__METHOD__ . ' Tag with name already exists', ['app' => 'core']); return false; } diff --git a/version.php b/version.php index 3d66ce636dc..0181f04155d 100644 --- a/version.php +++ b/version.php @@ -9,7 +9,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // when updating major/minor version number. -$OC_Version = [31, 0, 7, 1]; +$OC_Version = [31, 0, 7, 2]; // The human-readable string $OC_VersionString = '31.0.7'; |