Skip to content

Commit b482567

Browse files
authored
Add unique index for project_issue to prevent duplicate data (#30190)
Fix #27639
1 parent 8a5c597 commit b482567

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-
2+
id: 1
3+
project_id: 1
4+
issue_id: 1
5+
6+
-
7+
id: 2
8+
project_id: 1
9+
issue_id: 1

models/migrations/migrations.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"code.gitea.io/gitea/models/migrations/v1_20"
2222
"code.gitea.io/gitea/models/migrations/v1_21"
2323
"code.gitea.io/gitea/models/migrations/v1_22"
24+
"code.gitea.io/gitea/models/migrations/v1_23"
2425
"code.gitea.io/gitea/models/migrations/v1_6"
2526
"code.gitea.io/gitea/models/migrations/v1_7"
2627
"code.gitea.io/gitea/models/migrations/v1_8"
@@ -572,6 +573,10 @@ var migrations = []Migration{
572573
NewMigration("Ensure every project has exactly one default column - No Op", noopMigration),
573574
// v293 -> v294
574575
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
576+
577+
// Gitea 1.22.0 ends at 294
578+
579+
NewMigration("Add unique index for project issue table", v1_23.AddUniqueIndexForProjectIssue),
575580
}
576581

577582
// GetCurrentDBVersion returns the current db version

models/migrations/v1_23/main_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/migrations/base"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
base.MainTest(m)
14+
}

models/migrations/v1_23/v294.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import (
7+
"fmt"
8+
9+
"xorm.io/xorm"
10+
"xorm.io/xorm/schemas"
11+
)
12+
13+
// AddUniqueIndexForProjectIssue adds unique indexes for project issue table
14+
func AddUniqueIndexForProjectIssue(x *xorm.Engine) error {
15+
// remove possible duplicated records in table project_issue
16+
type result struct {
17+
IssueID int64
18+
ProjectID int64
19+
Cnt int
20+
}
21+
var results []result
22+
if err := x.Select("issue_id, project_id, count(*) as cnt").
23+
Table("project_issue").
24+
GroupBy("issue_id, project_id").
25+
Having("count(*) > 1").
26+
Find(&results); err != nil {
27+
return err
28+
}
29+
for _, r := range results {
30+
if x.Dialect().URI().DBType == schemas.MSSQL {
31+
if _, err := x.Exec(fmt.Sprintf("delete from project_issue where id in (SELECT top %d id FROM project_issue WHERE issue_id = ? and project_id = ?)", r.Cnt-1), r.IssueID, r.ProjectID); err != nil {
32+
return err
33+
}
34+
} else {
35+
var ids []int64
36+
if err := x.SQL("SELECT id FROM project_issue WHERE issue_id = ? and project_id = ? limit ?", r.IssueID, r.ProjectID, r.Cnt-1).Find(&ids); err != nil {
37+
return err
38+
}
39+
if _, err := x.Table("project_issue").In("id", ids).Delete(); err != nil {
40+
return err
41+
}
42+
}
43+
}
44+
45+
// add unique index for project_issue table
46+
type ProjectIssue struct { //revive:disable-line:exported
47+
ID int64 `xorm:"pk autoincr"`
48+
IssueID int64 `xorm:"INDEX unique(s)"`
49+
ProjectID int64 `xorm:"INDEX unique(s)"`
50+
}
51+
52+
return x.Sync(new(ProjectIssue))
53+
}

models/migrations/v1_23/v294_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_23 //nolint
5+
6+
import (
7+
"slices"
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/migrations/base"
11+
12+
"github.com/stretchr/testify/assert"
13+
"xorm.io/xorm/schemas"
14+
)
15+
16+
func Test_AddUniqueIndexForProjectIssue(t *testing.T) {
17+
type ProjectIssue struct { //revive:disable-line:exported
18+
ID int64 `xorm:"pk autoincr"`
19+
IssueID int64 `xorm:"INDEX"`
20+
ProjectID int64 `xorm:"INDEX"`
21+
}
22+
23+
// Prepare and load the testing database
24+
x, deferable := base.PrepareTestEnv(t, 0, new(ProjectIssue))
25+
defer deferable()
26+
if x == nil || t.Failed() {
27+
return
28+
}
29+
30+
cnt, err := x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
31+
assert.NoError(t, err)
32+
assert.EqualValues(t, 2, cnt)
33+
34+
assert.NoError(t, AddUniqueIndexForProjectIssue(x))
35+
36+
cnt, err = x.Table("project_issue").Where("project_id=1 AND issue_id=1").Count()
37+
assert.NoError(t, err)
38+
assert.EqualValues(t, 1, cnt)
39+
40+
tables, err := x.DBMetas()
41+
assert.NoError(t, err)
42+
assert.EqualValues(t, 1, len(tables))
43+
found := false
44+
for _, index := range tables[0].Indexes {
45+
if index.Type == schemas.UniqueType {
46+
found = true
47+
slices.Equal(index.Cols, []string{"project_id", "issue_id"})
48+
break
49+
}
50+
}
51+
assert.True(t, found)
52+
}

0 commit comments

Comments
 (0)