fix(core): ensure unique vcategory

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
pull/54165/head
skjnldsv 2 months ago committed by John Molakvoæ
parent 51e5f7b159
commit 9aac182109
  1. 6
      core/Listener/AddMissingIndicesListener.php
  2. 1
      core/Migrations/Version13000Date20170718121200.php
  3. 106
      core/Migrations/Version32000Date20250731062008.php
  4. 1
      lib/composer/composer/autoload_classmap.php
  5. 1
      lib/composer/composer/autoload_static.php
  6. 1
      lib/private/Tags.php
  7. 2
      version.php

@ -210,5 +210,11 @@ class AddMissingIndicesListener implements IEventListener {
'systag_objecttype',
['objecttype']
);
$event->addMissingUniqueIndex(
'vcategory',
'unique_category_per_user',
['uid', 'type', 'category']
);
}
}

@ -658,6 +658,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')) {

@ -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 Version32000Date20250731062008 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");
}
}
}

@ -1510,6 +1510,7 @@ return array(
'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',

@ -1551,6 +1551,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.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;
}

@ -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 = [32, 0, 0, 1];
$OC_Version = [32, 0, 0, 2];
// The human-readable string
$OC_VersionString = '32.0.0 dev';

Loading…
Cancel
Save