Skip to content

Commit b5326a4

Browse files
Optimization of labels handling in issue_search (go-gitea#26460)
This PR enhances the labels handling in issue_search by optimizing the SQL query and de-duplicate the IDs when generating the query string. --------- Co-authored-by: techknowlogick <[email protected]>
1 parent 72c66bd commit b5326a4

File tree

3 files changed

+59
-9
lines changed

3 files changed

+59
-9
lines changed

models/issues/issue_search.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ package issues
66
import (
77
"context"
88
"fmt"
9+
"strconv"
910
"strings"
1011

1112
"code.gitea.io/gitea/models/db"
1213
"code.gitea.io/gitea/models/organization"
1314
repo_model "code.gitea.io/gitea/models/repo"
1415
"code.gitea.io/gitea/models/unit"
1516
user_model "code.gitea.io/gitea/models/user"
17+
"code.gitea.io/gitea/modules/container"
1618
"code.gitea.io/gitea/modules/optional"
1719

1820
"xorm.io/builder"
@@ -116,14 +118,30 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
116118
if opts.LabelIDs[0] == 0 {
117119
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
118120
} else {
119-
for i, labelID := range opts.LabelIDs {
121+
// We sort and deduplicate the labels' ids
122+
IncludedLabelIDs := make(container.Set[int64])
123+
ExcludedLabelIDs := make(container.Set[int64])
124+
for _, labelID := range opts.LabelIDs {
120125
if labelID > 0 {
121-
sess.Join("INNER", fmt.Sprintf("issue_label il%d", i),
122-
fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID))
126+
IncludedLabelIDs.Add(labelID)
123127
} else if labelID < 0 { // 0 is not supported here, so just ignore it
124-
sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID)
128+
ExcludedLabelIDs.Add(-labelID)
125129
}
126130
}
131+
// ... and use them in a subquery of the form :
132+
// where (select count(*) from issue_label where issue_id=issue.id and label_id in (2, 4, 6)) = 3
133+
// This equality is guaranteed thanks to unique index (issue_id,label_id) on table issue_label.
134+
if len(IncludedLabelIDs) > 0 {
135+
subquery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
136+
And(builder.In("label_id", IncludedLabelIDs.Values()))
137+
sess.Where(builder.Eq{strconv.Itoa(len(IncludedLabelIDs)): subquery})
138+
}
139+
// or (select count(*)...) = 0 for excluded labels
140+
if len(ExcludedLabelIDs) > 0 {
141+
subquery := builder.Select("count(*)").From("issue_label").Where(builder.Expr("issue_id = issue.id")).
142+
And(builder.In("label_id", ExcludedLabelIDs.Values()))
143+
sess.Where(builder.Eq{"0": subquery})
144+
}
127145
}
128146
}
129147

models/issues/label.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package issues
77
import (
88
"context"
99
"fmt"
10+
"slices"
1011
"strconv"
1112
"strings"
1213

@@ -142,9 +143,8 @@ func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
142143

143144
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
144145
func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
145-
var labelQuerySlice []string
146+
labelQuerySlice := []int64{}
146147
labelSelected := false
147-
labelID := strconv.FormatInt(l.ID, 10)
148148
labelScope := l.ExclusiveScope()
149149
for i, s := range currentSelectedLabels {
150150
if s == l.ID {
@@ -155,15 +155,26 @@ func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, curr
155155
} else if s != 0 {
156156
// Exclude other labels in the same scope from selection
157157
if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
158-
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
158+
labelQuerySlice = append(labelQuerySlice, s)
159159
}
160160
}
161161
}
162+
162163
if !labelSelected {
163-
labelQuerySlice = append(labelQuerySlice, labelID)
164+
labelQuerySlice = append(labelQuerySlice, l.ID)
164165
}
165166
l.IsSelected = labelSelected
166-
l.QueryString = strings.Join(labelQuerySlice, ",")
167+
168+
// Sort and deduplicate the ids to avoid the crawlers asking for the
169+
// same thing with simply a different order of parameters
170+
slices.Sort(labelQuerySlice)
171+
labelQuerySlice = slices.Compact(labelQuerySlice)
172+
// Quick conversion (strings.Join() doesn't accept slices of Int64)
173+
labelQuerySliceStrings := make([]string, len(labelQuerySlice))
174+
for i, x := range labelQuerySlice {
175+
labelQuerySliceStrings[i] = strconv.FormatInt(x, 10)
176+
}
177+
l.QueryString = strings.Join(labelQuerySliceStrings, ",")
167178
}
168179

169180
// BelongsToOrg returns true if label is an organization label

models/issues/label_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ func TestLabel_CalOpenIssues(t *testing.T) {
2323
assert.EqualValues(t, 2, label.NumOpenIssues)
2424
}
2525

26+
func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) {
27+
assert.NoError(t, unittest.PrepareTestDatabase())
28+
// Loading the label id:8 which have a scope and an exclusivity
29+
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8})
30+
31+
// First test : with negative and scope
32+
label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"})
33+
assert.Equal(t, "1", label.QueryString)
34+
assert.Equal(t, true, label.IsSelected)
35+
36+
// Second test : with duplicates
37+
label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"})
38+
assert.Equal(t, "1,8", label.QueryString)
39+
assert.Equal(t, false, label.IsSelected)
40+
41+
// Third test : empty set
42+
label.LoadSelectedLabelsAfterClick([]int64{}, []string{})
43+
assert.False(t, label.IsSelected)
44+
assert.Equal(t, "8", label.QueryString)
45+
}
46+
2647
func TestLabel_ExclusiveScope(t *testing.T) {
2748
assert.NoError(t, unittest.PrepareTestDatabase())
2849
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})

0 commit comments

Comments
 (0)