From e08f1a9cbd582c73918e401eeba36261627f44a7 Mon Sep 17 00:00:00 2001
From: sebastian-sauer <sauer.sebastian@gmail.com>
Date: Thu, 14 Dec 2023 10:26:59 +0100
Subject: [PATCH] Add combined index for issue_user.uid and issue_id (#28080)

fixes #27877

---------

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/issues/issue_user.go                   |  4 +--
 .../issue_user.yml                            | 20 +++++++++++
 models/migrations/migrations.go               |  2 ++
 models/migrations/v1_22/main_test.go          | 14 ++++++++
 models/migrations/v1_22/v283.go               | 34 +++++++++++++++++++
 models/migrations/v1_22/v283_test.go          | 28 +++++++++++++++
 6 files changed, 100 insertions(+), 2 deletions(-)
 create mode 100644 models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml
 create mode 100644 models/migrations/v1_22/main_test.go
 create mode 100644 models/migrations/v1_22/v283.go
 create mode 100644 models/migrations/v1_22/v283_test.go

diff --git a/models/issues/issue_user.go b/models/issues/issue_user.go
index 24bb74648d..6b59e0725e 100644
--- a/models/issues/issue_user.go
+++ b/models/issues/issue_user.go
@@ -14,8 +14,8 @@ import (
 // IssueUser represents an issue-user relation.
 type IssueUser struct {
 	ID          int64 `xorm:"pk autoincr"`
-	UID         int64 `xorm:"INDEX"` // User ID.
-	IssueID     int64 `xorm:"INDEX"`
+	UID         int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+	IssueID     int64 `xorm:"INDEX unique(uid_to_issue)"`
 	IsRead      bool
 	IsMentioned bool
 }
diff --git a/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml b/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml
new file mode 100644
index 0000000000..7bbb6f2f30
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml
@@ -0,0 +1,20 @@
+-
+  id: 1
+  uid: 1
+  issue_id: 1
+  is_read: true
+  is_mentioned: false
+
+-
+  id: 2
+  uid: 2
+  issue_id: 1
+  is_read: true
+  is_mentioned: false
+
+-
+  id: 3
+  uid: 2
+  issue_id: 1 # duplicated with id 2
+  is_read: false
+  is_mentioned: true
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 28e3be503b..578cbca035 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -550,6 +550,8 @@ var migrations = []Migration{
 	NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
 	// v282 -> v283
 	NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID),
+	// v283 -> v284
+	NewMigration("Add combined Index to issue_user.uid and issue_id", v1_22.AddCombinedIndexToIssueUser),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go
new file mode 100644
index 0000000000..efd8dbaa8c
--- /dev/null
+++ b/models/migrations/v1_22/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func TestMain(m *testing.M) {
+	base.MainTest(m)
+}
diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go
new file mode 100644
index 0000000000..b2b94845d9
--- /dev/null
+++ b/models/migrations/v1_22/v283.go
@@ -0,0 +1,34 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
+	type OldIssueUser struct {
+		IssueID int64
+		UID     int64
+		Cnt     int64
+	}
+
+	var duplicatedIssueUsers []OldIssueUser
+	if err := x.SQL("select * from (select issue_id, uid, count(1) as cnt from issue_user group by issue_id, uid) a where a.cnt > 1").
+		Find(&duplicatedIssueUsers); err != nil {
+		return err
+	}
+	for _, issueUser := range duplicatedIssueUsers {
+		if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
+			return err
+		}
+	}
+
+	type IssueUser struct {
+		UID     int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+		IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
+	}
+
+	return x.Sync(&IssueUser{})
+}
diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go
new file mode 100644
index 0000000000..864f47f840
--- /dev/null
+++ b/models/migrations/v1_22/v283_test.go
@@ -0,0 +1,28 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func Test_AddCombinedIndexToIssueUser(t *testing.T) {
+	type IssueUser struct {
+		UID     int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+		IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	if err := AddCombinedIndexToIssueUser(x); err != nil {
+		t.Fatal(err)
+	}
+}