From f45e72949b34fb0f9fccfcd1f3d2d5e00ad2dc53 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Thu, 15 Jan 2026 21:24:05 +0100 Subject: [PATCH] Fix invitations error: Closes #1833 --- ..._15_000000_fix_invitations_foreign_key.php | 190 ++++++++++++++++++ ...001_cleanup_invitations_legacy_columns.php | 40 ++++ 2 files changed, 230 insertions(+) create mode 100644 database/migrations/2026_01_15_000000_fix_invitations_foreign_key.php create mode 100644 database/migrations/2026_01_15_000001_cleanup_invitations_legacy_columns.php diff --git a/database/migrations/2026_01_15_000000_fix_invitations_foreign_key.php b/database/migrations/2026_01_15_000000_fix_invitations_foreign_key.php new file mode 100644 index 000000000..8acb26d66 --- /dev/null +++ b/database/migrations/2026_01_15_000000_fix_invitations_foreign_key.php @@ -0,0 +1,190 @@ +dropForeignKeySafely('invitations', 'FK_users_inv'); + + Schema::table('invitations', function (Blueprint $table) { + // Rename users_id to invited_by if needed + if (Schema::hasColumn('invitations', 'users_id') && ! Schema::hasColumn('invitations', 'invited_by')) { + $table->renameColumn('users_id', 'invited_by'); + } + }); + + // Now add missing columns in a separate Schema call (after rename) + Schema::table('invitations', function (Blueprint $table) { + // Drop the old guid column if it exists (replaced by token) + if (Schema::hasColumn('invitations', 'guid') && Schema::hasColumn('invitations', 'token')) { + $table->dropColumn('guid'); + } + + // Add token if it doesn't exist + if (! Schema::hasColumn('invitations', 'token')) { + $table->string('token', 64)->unique()->after('id'); + } + + // Add email if it doesn't exist + if (! Schema::hasColumn('invitations', 'email')) { + $table->string('email')->after('token'); + } + + // Add expires_at if it doesn't exist + if (! Schema::hasColumn('invitations', 'expires_at')) { + if (Schema::hasColumn('invitations', 'invited_by')) { + $table->timestamp('expires_at')->after('invited_by'); + } else { + $table->timestamp('expires_at')->nullable(); + } + } + + // Add used_at if it doesn't exist + if (! Schema::hasColumn('invitations', 'used_at')) { + $table->timestamp('used_at')->nullable(); + } + + // Add used_by if it doesn't exist + if (! Schema::hasColumn('invitations', 'used_by')) { + $table->unsignedBigInteger('used_by')->nullable(); + } + + // Add is_active if it doesn't exist + if (! Schema::hasColumn('invitations', 'is_active')) { + $table->boolean('is_active')->default(true); + } + + // Add metadata if it doesn't exist + if (! Schema::hasColumn('invitations', 'metadata')) { + $table->json('metadata')->nullable(); + } + }); + + // Migrate data from guid to token if guid still exists and has data + if (Schema::hasColumn('invitations', 'guid') && Schema::hasColumn('invitations', 'token')) { + DB::table('invitations') + ->whereNull('token') + ->orWhere('token', '') + ->update(['token' => DB::raw('guid')]); + + // Now drop guid + Schema::table('invitations', function (Blueprint $table) { + $table->dropColumn('guid'); + }); + } + + // Add indexes safely + $this->addIndexSafely('invitations', ['invited_by'], 'invitations_invited_by_index'); + $this->addIndexSafely('invitations', ['used_by'], 'invitations_used_by_index'); + $this->addIndexSafely('invitations', ['token', 'is_active'], 'invitations_token_is_active_index'); + $this->addIndexSafely('invitations', ['email', 'is_active'], 'invitations_email_is_active_index'); + $this->addIndexSafely('invitations', ['expires_at'], 'invitations_expires_at_index'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Drop indexes first + $this->dropIndexSafely('invitations', 'invitations_invited_by_index'); + $this->dropIndexSafely('invitations', 'invitations_used_by_index'); + $this->dropIndexSafely('invitations', 'invitations_token_is_active_index'); + $this->dropIndexSafely('invitations', 'invitations_email_is_active_index'); + $this->dropIndexSafely('invitations', 'invitations_expires_at_index'); + + Schema::table('invitations', function (Blueprint $table) { + // Rename back to original column name + if (Schema::hasColumn('invitations', 'invited_by') && ! Schema::hasColumn('invitations', 'users_id')) { + $table->renameColumn('invited_by', 'users_id'); + } + }); + + // Re-add the foreign key constraint + Schema::table('invitations', function (Blueprint $table) { + if (Schema::hasColumn('invitations', 'users_id')) { + $table->foreign('users_id', 'FK_users_inv') + ->references('id') + ->on('users') + ->onDelete('cascade') + ->onUpdate('cascade'); + } + }); + } + + /** + * Safely drop a foreign key if it exists + */ + private function dropForeignKeySafely(string $table, string $foreignKeyName): void + { + try { + $foreignKeys = DB::select( + "SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' + AND TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND CONSTRAINT_NAME = ?", + [$table, $foreignKeyName] + ); + + if (! empty($foreignKeys)) { + Schema::table($table, function (Blueprint $table) use ($foreignKeyName) { + $table->dropForeign($foreignKeyName); + }); + } + } catch (\Exception $e) { + // Foreign key might not exist, continue + report($e); + } + } + + /** + * Safely add an index if it doesn't exist + */ + private function addIndexSafely(string $table, array $columns, string $indexName): void + { + try { + $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); + if (empty($indexes)) { + Schema::table($table, function (Blueprint $table) use ($columns, $indexName) { + $table->index($columns, $indexName); + }); + } + } catch (\Exception $e) { + // Index creation failed, but continue + report($e); + } + } + + /** + * Safely drop an index if it exists + */ + private function dropIndexSafely(string $table, string $indexName): void + { + try { + $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); + if (! empty($indexes)) { + Schema::table($table, function (Blueprint $table) use ($indexName) { + $table->dropIndex($indexName); + }); + } + } catch (\Exception $e) { + // Index drop failed, but continue + report($e); + } + } +}; diff --git a/database/migrations/2026_01_15_000001_cleanup_invitations_legacy_columns.php b/database/migrations/2026_01_15_000001_cleanup_invitations_legacy_columns.php new file mode 100644 index 000000000..fa43bbd51 --- /dev/null +++ b/database/migrations/2026_01_15_000001_cleanup_invitations_legacy_columns.php @@ -0,0 +1,40 @@ +dropColumn('users_id'); + } + + // Drop guid column if it still exists (we use token now) + if (Schema::hasColumn('invitations', 'guid')) { + $table->dropColumn('guid'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invitations', function (Blueprint $table) { + if (! Schema::hasColumn('invitations', 'users_id')) { + $table->unsignedInteger('users_id')->nullable()->after('id'); + } + }); + } +};