From 8b489b9cc3919dedf1463c7c7dd54e7a8009fc6f Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Wed, 7 Jan 2026 22:25:24 +0200 Subject: [PATCH] fix: forum users tables migrations --- lib/Db/ForumUser.php | 1 - lib/Migration/SeedHelper.php | 20 +++-- lib/Migration/Version1Date20251106004226.php | 34 ++++++-- lib/Migration/Version2Date20251114222614.php | 85 ++++++++++++++++---- lib/Migration/Version8Date20251128000000.php | 15 +++- 5 files changed, 126 insertions(+), 29 deletions(-) diff --git a/lib/Db/ForumUser.php b/lib/Db/ForumUser.php index dda1633..30d5bd7 100644 --- a/lib/Db/ForumUser.php +++ b/lib/Db/ForumUser.php @@ -31,7 +31,6 @@ use OCP\AppFramework\Db\Entity; * @method void setUpdatedAt(int $updatedAt) */ class ForumUser extends Entity implements JsonSerializable { - public $id; protected string $userId = ''; protected int $postCount = 0; protected int $threadCount = 0; diff --git a/lib/Migration/SeedHelper.php b/lib/Migration/SeedHelper.php index ac7af82..bc15001 100644 --- a/lib/Migration/SeedHelper.php +++ b/lib/Migration/SeedHelper.php @@ -125,7 +125,8 @@ class SeedHelper { /** * Create the forum_users table from scratch - * This mirrors the schema from Version1 + Version8 migrations + * This mirrors the final schema from Version1 + Version2 + Version8 migrations + * (id as primary key, user_id as unique, includes signature column) */ private static function createForumUsersTable(\OCP\IDBConnection $db): void { $platform = $db->getDatabasePlatform(); @@ -138,6 +139,7 @@ class SeedHelper { // MySQL and MariaDB both extend MySQLPlatform $db->executeStatement(" CREATE TABLE `{$tableName}` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` VARCHAR(64) NOT NULL, `post_count` INT UNSIGNED NOT NULL DEFAULT 0, `thread_count` INT UNSIGNED NOT NULL DEFAULT 0, @@ -146,14 +148,17 @@ class SeedHelper { `signature` TEXT DEFAULT NULL, `created_at` INT UNSIGNED NOT NULL, `updated_at` INT UNSIGNED NOT NULL, - PRIMARY KEY (`user_id`), + PRIMARY KEY (`id`), + UNIQUE INDEX `forum_users_user_id_uniq` (`user_id`), INDEX `forum_users_post_count_idx` (`post_count`), + INDEX `forum_users_thread_count_idx` (`thread_count`), INDEX `forum_users_deleted_at_idx` (`deleted_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin "); } elseif ($platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) { $db->executeStatement(" CREATE TABLE \"{$tableName}\" ( + \"id\" BIGSERIAL PRIMARY KEY, \"user_id\" VARCHAR(64) NOT NULL, \"post_count\" INTEGER NOT NULL DEFAULT 0, \"thread_count\" INTEGER NOT NULL DEFAULT 0, @@ -161,16 +166,18 @@ class SeedHelper { \"deleted_at\" INTEGER DEFAULT NULL, \"signature\" TEXT DEFAULT NULL, \"created_at\" INTEGER NOT NULL, - \"updated_at\" INTEGER NOT NULL, - PRIMARY KEY (\"user_id\") + \"updated_at\" INTEGER NOT NULL ) "); + $db->executeStatement("CREATE UNIQUE INDEX \"forum_users_user_id_uniq\" ON \"{$tableName}\" (\"user_id\")"); $db->executeStatement("CREATE INDEX \"forum_users_post_count_idx\" ON \"{$tableName}\" (\"post_count\")"); + $db->executeStatement("CREATE INDEX \"forum_users_thread_count_idx\" ON \"{$tableName}\" (\"thread_count\")"); $db->executeStatement("CREATE INDEX \"forum_users_deleted_at_idx\" ON \"{$tableName}\" (\"deleted_at\")"); } else { // SQLite (and any other platform as fallback) $db->executeStatement(" CREATE TABLE \"{$tableName}\" ( + \"id\" INTEGER PRIMARY KEY AUTOINCREMENT, \"user_id\" VARCHAR(64) NOT NULL, \"post_count\" INTEGER NOT NULL DEFAULT 0, \"thread_count\" INTEGER NOT NULL DEFAULT 0, @@ -178,11 +185,12 @@ class SeedHelper { \"deleted_at\" INTEGER DEFAULT NULL, \"signature\" TEXT DEFAULT NULL, \"created_at\" INTEGER NOT NULL, - \"updated_at\" INTEGER NOT NULL, - PRIMARY KEY (\"user_id\") + \"updated_at\" INTEGER NOT NULL ) "); + $db->executeStatement("CREATE UNIQUE INDEX \"forum_users_user_id_uniq\" ON \"{$tableName}\" (\"user_id\")"); $db->executeStatement("CREATE INDEX \"forum_users_post_count_idx\" ON \"{$tableName}\" (\"post_count\")"); + $db->executeStatement("CREATE INDEX \"forum_users_thread_count_idx\" ON \"{$tableName}\" (\"thread_count\")"); $db->executeStatement("CREATE INDEX \"forum_users_deleted_at_idx\" ON \"{$tableName}\" (\"deleted_at\")"); } } diff --git a/lib/Migration/Version1Date20251106004226.php b/lib/Migration/Version1Date20251106004226.php index ad9d3c2..6548491 100644 --- a/lib/Migration/Version1Date20251106004226.php +++ b/lib/Migration/Version1Date20251106004226.php @@ -85,12 +85,30 @@ class Version1Date20251106004226 extends SimpleMigrationStep { $table->addIndex(['name'], 'forum_roles_name_idx'); } + /** + * Create forum_users table (formerly forum_user_stats) + * Note: On fresh installs, this creates forum_users directly with the final schema. + * For progressive installs where forum_user_stats already exists, + * SeedHelper::ensureForumUsersTable() handles the rename. + * + * The table structure matches what Version2 transforms it to: + * - id: auto-increment primary key + * - user_id: unique string + * - signature: added in Version8 + */ private function createUserStatsTable(ISchemaWrapper $schema): void { - if ($schema->hasTable('forum_user_stats')) { + // Skip if either table already exists (handles both fresh and progressive installs) + if ($schema->hasTable('forum_users') || $schema->hasTable('forum_user_stats')) { return; } - $table = $schema->createTable('forum_user_stats'); + // Create forum_users directly with the final schema (matching Version2's transformation) + $table = $schema->createTable('forum_users'); + $table->addColumn('id', 'bigint', [ + 'autoincrement' => true, + 'notnull' => true, + 'unsigned' => true, + ]); $table->addColumn('user_id', 'string', [ 'notnull' => true, 'length' => 64, @@ -115,6 +133,10 @@ class Version1Date20251106004226 extends SimpleMigrationStep { 'unsigned' => true, 'default' => null, ]); + $table->addColumn('signature', 'text', [ + 'notnull' => false, + 'default' => null, + ]); $table->addColumn('created_at', 'integer', [ 'notnull' => true, 'unsigned' => true, @@ -123,9 +145,11 @@ class Version1Date20251106004226 extends SimpleMigrationStep { 'notnull' => true, 'unsigned' => true, ]); - $table->setPrimaryKey(['user_id']); - $table->addIndex(['post_count'], 'user_stats_post_count_idx'); - $table->addIndex(['deleted_at'], 'user_stats_deleted_at_idx'); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['user_id'], 'forum_users_user_id_uniq'); + $table->addIndex(['post_count'], 'forum_users_post_count_idx'); + $table->addIndex(['thread_count'], 'forum_users_thread_count_idx'); + $table->addIndex(['deleted_at'], 'forum_users_deleted_at_idx'); } private function createForumUserRolesTable(ISchemaWrapper $schema): void { diff --git a/lib/Migration/Version2Date20251114222614.php b/lib/Migration/Version2Date20251114222614.php index b3b5c3b..9fcf44a 100644 --- a/lib/Migration/Version2Date20251114222614.php +++ b/lib/Migration/Version2Date20251114222614.php @@ -67,18 +67,38 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $table->addUniqueIndex(['user_id', 'thread_id'], 'thread_subs_uniq_idx'); } + /** + * Fix forum_user_stats or forum_users table structure + * Handles both old table name (progressive installs) and new table name (fresh installs) + */ private function fixForumUserStatsTable(ISchemaWrapper $schema): void { - if (!$schema->hasTable('forum_user_stats')) { + // Determine which table exists (handles both fresh and progressive installs) + $tableName = null; + if ($schema->hasTable('forum_user_stats')) { + $tableName = 'forum_user_stats'; + } elseif ($schema->hasTable('forum_users')) { + $tableName = 'forum_users'; + } + + if ($tableName === null) { return; } - $table = $schema->getTable('forum_user_stats'); + $table = $schema->getTable($tableName); // Check if already fixed (has id column) + // Note: On fresh installs, forum_users uses user_id as primary key (no id column needed) + // This fix is only needed for progressive installs with old forum_user_stats structure if ($table->hasColumn('id')) { return; } + // Only add id column to forum_user_stats (old structure) + // forum_users created in Version1 uses user_id as primary key and doesn't need this fix + if ($tableName !== 'forum_user_stats') { + return; + } + // Add id column as auto-increment $table->addColumn('id', 'bigint', [ 'autoincrement' => true, @@ -123,8 +143,7 @@ class Version2Date20251114222614 extends SimpleMigrationStep { } /** - * Rebuild user stats using the old table name (forum_user_stats) - * This is needed because Version8 hasn't renamed the table yet + * Rebuild user stats - handles both old (forum_user_stats) and new (forum_users) table names */ private function rebuildAllUserStatsLegacy(): array { // Get all user IDs from Nextcloud @@ -133,11 +152,22 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $users[] = $user->getUID(); }); + // Determine which table to use + $tableName = $this->getUserStatsTableName(); + if ($tableName === null) { + // No table exists yet - this shouldn't happen but handle gracefully + return [ + 'users' => count($users), + 'updated' => 0, + 'created' => 0, + ]; + } + $updated = 0; $created = 0; foreach ($users as $userId) { - $wasCreated = $this->rebuildUserStatsLegacy($userId); + $wasCreated = $this->rebuildUserStatsLegacy($userId, $tableName); if ($wasCreated) { $created++; } else { @@ -153,9 +183,36 @@ class Version2Date20251114222614 extends SimpleMigrationStep { } /** - * Rebuild stats for a single user using the old table name + * Get the user stats table name (handles both old and new names) */ - private function rebuildUserStatsLegacy(string $userId): bool { + private function getUserStatsTableName(): ?string { + // Check forum_users first (new name, for fresh installs) + // Then check forum_user_stats (old name, for progressive installs) + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('user_id')->from('forum_users')->setMaxResults(1); + $qb->executeQuery()->closeCursor(); + return 'forum_users'; + } catch (\Exception $e) { + // Table doesn't exist, try old name + } + + try { + $qb = $this->db->getQueryBuilder(); + $qb->select('user_id')->from('forum_user_stats')->setMaxResults(1); + $qb->executeQuery()->closeCursor(); + return 'forum_user_stats'; + } catch (\Exception $e) { + // Neither table exists + } + + return null; + } + + /** + * Rebuild stats for a single user + */ + private function rebuildUserStatsLegacy(string $userId, string $tableName): bool { // Count non-deleted threads created by this user $threadQb = $this->db->getQueryBuilder(); $threadQb->select($threadQb->func()->count('*', 'count')) @@ -194,10 +251,10 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $lastPostAt = $lastPostResult->fetchOne(); $lastPostResult->closeCursor(); - // Check if forum user record already exists (using OLD table name) + // Check if forum user record already exists $checkQb = $this->db->getQueryBuilder(); $checkQb->select('user_id') - ->from('forum_user_stats') // OLD table name! + ->from($tableName) ->where($checkQb->expr()->eq('user_id', $checkQb->createNamedParameter($userId))); $checkResult = $checkQb->executeQuery(); $exists = $checkResult->fetch(); @@ -206,9 +263,9 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $timestamp = time(); if ($exists) { - // Update existing record (using OLD table name) + // Update existing record $updateQb = $this->db->getQueryBuilder(); - $updateQb->update('forum_user_stats') // OLD table name! + $updateQb->update($tableName) ->set('thread_count', $updateQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT)) ->set('post_count', $updateQb->createNamedParameter($postCount, IQueryBuilder::PARAM_INT)) ->set('updated_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) @@ -221,9 +278,9 @@ class Version2Date20251114222614 extends SimpleMigrationStep { $updateQb->executeStatement(); return false; } else { - // Create new record (using OLD table name) + // Create new record $insertQb = $this->db->getQueryBuilder(); - $insertQb->insert('forum_user_stats') // OLD table name! + $insertQb->insert($tableName) ->values([ 'user_id' => $insertQb->createNamedParameter($userId), 'thread_count' => $insertQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT), @@ -239,7 +296,7 @@ class Version2Date20251114222614 extends SimpleMigrationStep { } catch (\Exception $e) { // If insert fails (race condition), try updating instead $updateQb = $this->db->getQueryBuilder(); - $updateQb->update('forum_user_stats') // OLD table name! + $updateQb->update($tableName) ->set('thread_count', $updateQb->createNamedParameter($threadCount, IQueryBuilder::PARAM_INT)) ->set('post_count', $updateQb->createNamedParameter($postCount, IQueryBuilder::PARAM_INT)) ->set('updated_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) diff --git a/lib/Migration/Version8Date20251128000000.php b/lib/Migration/Version8Date20251128000000.php index 8cc6be1..e6998ce 100644 --- a/lib/Migration/Version8Date20251128000000.php +++ b/lib/Migration/Version8Date20251128000000.php @@ -43,9 +43,18 @@ class Version8Date20251128000000 extends SimpleMigrationStep { } } - // Add signature column to user stats - if ($schema->hasTable('forum_user_stats')) { - $table = $schema->getTable('forum_user_stats'); + // Add signature column to forum_users table (handles both old and new table names) + // On fresh installs: forum_users is created with signature column in Version1 + // On progressive installs: forum_user_stats may still exist and needs signature added + $userTableName = null; + if ($schema->hasTable('forum_users')) { + $userTableName = 'forum_users'; + } elseif ($schema->hasTable('forum_user_stats')) { + $userTableName = 'forum_user_stats'; + } + + if ($userTableName !== null) { + $table = $schema->getTable($userTableName); if (!$table->hasColumn('signature')) { $table->addColumn('signature', 'text', [