mirror of https://github.com/go-gitea/gitea.git
Browse Source
Noticed a SQL in gitea.com has a bigger load. It seems both `is_pull` and `pin_order` are not indexed columns in the database. ```SQL SELECT `id`, `repo_id`, `index`, `poster_id`, `original_author`, `original_author_id`, `name`, `content`, `content_version`, `milestone_id`, `priority`, `is_closed`, `is_pull`, `num_comments`, `ref`, `pin_order`, `deadline_unix`, `created_unix`, `updated_unix`, `closed_unix`, `is_locked`, `time_estimate` FROM `issue` WHERE (repo_id =?) AND (is_pull = 0) AND (pin_order > 0) ORDER BY pin_order ``` I came across a comment https://github.com/go-gitea/gitea/pull/24406#issuecomment-1527747296 from @delvh , which presents a more reasonable approach. Based on this, this PR will migrate all issue and pull request pin data from the `issue` table to the `issue_pin` table. This change benefits larger Gitea instances by improving scalability and performance. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/33633/head
13 changed files with 396 additions and 218 deletions
@ -0,0 +1,6 @@ |
|||||||
|
- |
||||||
|
id: 1 |
||||||
|
repo_id: 2 |
||||||
|
issue_id: 4 |
||||||
|
is_pull: false |
||||||
|
pin_order: 1 |
||||||
@ -0,0 +1,246 @@ |
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"sort" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db" |
||||||
|
user_model "code.gitea.io/gitea/models/user" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/gitea/modules/util" |
||||||
|
) |
||||||
|
|
||||||
|
type IssuePin struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
IssueID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
IsPull bool `xorm:"NOT NULL"` |
||||||
|
PinOrder int `xorm:"DEFAULT 0"` |
||||||
|
} |
||||||
|
|
||||||
|
var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched") |
||||||
|
|
||||||
|
// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
|
||||||
|
func IsErrIssueMaxPinReached(err error) bool { |
||||||
|
return err == ErrIssueMaxPinReached |
||||||
|
} |
||||||
|
|
||||||
|
func init() { |
||||||
|
db.RegisterModel(new(IssuePin)) |
||||||
|
} |
||||||
|
|
||||||
|
func GetIssuePin(ctx context.Context, issue *Issue) (*IssuePin, error) { |
||||||
|
pin := new(IssuePin) |
||||||
|
has, err := db.GetEngine(ctx). |
||||||
|
Where("repo_id = ?", issue.RepoID). |
||||||
|
And("issue_id = ?", issue.ID).Get(pin) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} else if !has { |
||||||
|
return nil, db.ErrNotExist{ |
||||||
|
Resource: "IssuePin", |
||||||
|
ID: issue.ID, |
||||||
|
} |
||||||
|
} |
||||||
|
return pin, nil |
||||||
|
} |
||||||
|
|
||||||
|
func GetIssuePinsByIssueIDs(ctx context.Context, issueIDs []int64) ([]IssuePin, error) { |
||||||
|
var pins []IssuePin |
||||||
|
if err := db.GetEngine(ctx).In("issue_id", issueIDs).Find(&pins); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return pins, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Pin pins a Issue
|
||||||
|
func PinIssue(ctx context.Context, issue *Issue, user *user_model.User) error { |
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error { |
||||||
|
pinnedIssuesNum, err := getPinnedIssuesNum(ctx, issue.RepoID, issue.IsPull) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Check if the maximum allowed Pins reached
|
||||||
|
if pinnedIssuesNum >= setting.Repository.Issue.MaxPinned { |
||||||
|
return ErrIssueMaxPinReached |
||||||
|
} |
||||||
|
|
||||||
|
pinnedIssuesMaxPinOrder, err := getPinnedIssuesMaxPinOrder(ctx, issue.RepoID, issue.IsPull) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if _, err = db.GetEngine(ctx).Insert(&IssuePin{ |
||||||
|
RepoID: issue.RepoID, |
||||||
|
IssueID: issue.ID, |
||||||
|
IsPull: issue.IsPull, |
||||||
|
PinOrder: pinnedIssuesMaxPinOrder + 1, |
||||||
|
}); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Add the pin event to the history
|
||||||
|
_, err = CreateComment(ctx, &CreateCommentOptions{ |
||||||
|
Type: CommentTypePin, |
||||||
|
Doer: user, |
||||||
|
Repo: issue.Repo, |
||||||
|
Issue: issue, |
||||||
|
}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// UnpinIssue unpins a Issue
|
||||||
|
func UnpinIssue(ctx context.Context, issue *Issue, user *user_model.User) error { |
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error { |
||||||
|
// This sets the Pin for all Issues that come after the unpined Issue to the correct value
|
||||||
|
cnt, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Delete(new(IssuePin)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if cnt == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Add the unpin event to the history
|
||||||
|
_, err = CreateComment(ctx, &CreateCommentOptions{ |
||||||
|
Type: CommentTypeUnpin, |
||||||
|
Doer: user, |
||||||
|
Repo: issue.Repo, |
||||||
|
Issue: issue, |
||||||
|
}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func getPinnedIssuesNum(ctx context.Context, repoID int64, isPull bool) (int, error) { |
||||||
|
var pinnedIssuesNum int |
||||||
|
_, err := db.GetEngine(ctx).SQL("SELECT count(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&pinnedIssuesNum) |
||||||
|
return pinnedIssuesNum, err |
||||||
|
} |
||||||
|
|
||||||
|
func getPinnedIssuesMaxPinOrder(ctx context.Context, repoID int64, isPull bool) (int, error) { |
||||||
|
var maxPinnedIssuesMaxPinOrder int |
||||||
|
_, err := db.GetEngine(ctx).SQL("SELECT max(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPinnedIssuesMaxPinOrder) |
||||||
|
return maxPinnedIssuesMaxPinOrder, err |
||||||
|
} |
||||||
|
|
||||||
|
// MovePin moves a Pinned Issue to a new Position
|
||||||
|
func MovePin(ctx context.Context, issue *Issue, newPosition int) error { |
||||||
|
if newPosition < 1 { |
||||||
|
return errors.New("The Position can't be lower than 1") |
||||||
|
} |
||||||
|
|
||||||
|
issuePin, err := GetIssuePin(ctx, issue) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if issuePin.PinOrder == newPosition { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return db.WithTx(ctx, func(ctx context.Context) error { |
||||||
|
if issuePin.PinOrder > newPosition { // move the issue to a lower position
|
||||||
|
_, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ? AND pin_order < ?", issue.RepoID, issue.IsPull, newPosition, issuePin.PinOrder) |
||||||
|
} else { // move the issue to a higher position
|
||||||
|
// Lower the Position of all Pinned Issue that came after the current Position
|
||||||
|
_, err = db.GetEngine(ctx).Exec("UPDATE issue_pin SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ? AND pin_order <= ?", issue.RepoID, issue.IsPull, issuePin.PinOrder, newPosition) |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
_, err = db.GetEngine(ctx). |
||||||
|
Table("issue_pin"). |
||||||
|
Where("id = ?", issuePin.ID). |
||||||
|
Update(map[string]any{ |
||||||
|
"pin_order": newPosition, |
||||||
|
}) |
||||||
|
return err |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func GetPinnedIssueIDs(ctx context.Context, repoID int64, isPull bool) ([]int64, error) { |
||||||
|
var issuePins []IssuePin |
||||||
|
if err := db.GetEngine(ctx). |
||||||
|
Table("issue_pin"). |
||||||
|
Where("repo_id = ?", repoID). |
||||||
|
And("is_pull = ?", isPull). |
||||||
|
Find(&issuePins); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
sort.Slice(issuePins, func(i, j int) bool { |
||||||
|
return issuePins[i].PinOrder < issuePins[j].PinOrder |
||||||
|
}) |
||||||
|
|
||||||
|
var ids []int64 |
||||||
|
for _, pin := range issuePins { |
||||||
|
ids = append(ids, pin.IssueID) |
||||||
|
} |
||||||
|
return ids, nil |
||||||
|
} |
||||||
|
|
||||||
|
func GetIssuePinsByRepoID(ctx context.Context, repoID int64, isPull bool) ([]*IssuePin, error) { |
||||||
|
var pins []*IssuePin |
||||||
|
if err := db.GetEngine(ctx).Where("repo_id = ? AND is_pull = ?", repoID, isPull).Find(&pins); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return pins, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetPinnedIssues returns the pinned Issues for the given Repo and type
|
||||||
|
func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) { |
||||||
|
issuePins, err := GetIssuePinsByRepoID(ctx, repoID, isPull) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if len(issuePins) == 0 { |
||||||
|
return IssueList{}, nil |
||||||
|
} |
||||||
|
ids := make([]int64, 0, len(issuePins)) |
||||||
|
for _, pin := range issuePins { |
||||||
|
ids = append(ids, pin.IssueID) |
||||||
|
} |
||||||
|
|
||||||
|
issues := make(IssueList, 0, len(ids)) |
||||||
|
if err := db.GetEngine(ctx).In("id", ids).Find(&issues); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
for _, issue := range issues { |
||||||
|
for _, pin := range issuePins { |
||||||
|
if pin.IssueID == issue.ID { |
||||||
|
issue.PinOrder = pin.PinOrder |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if (!setting.IsProd || setting.IsInTesting) && issue.PinOrder == 0 { |
||||||
|
panic("It should not happen that a pinned Issue has no PinOrder") |
||||||
|
} |
||||||
|
} |
||||||
|
sort.Slice(issues, func(i, j int) bool { |
||||||
|
return issues[i].PinOrder < issues[j].PinOrder |
||||||
|
}) |
||||||
|
|
||||||
|
if err = issues.LoadAttributes(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return issues, nil |
||||||
|
} |
||||||
|
|
||||||
|
// IsNewPinAllowed returns if a new Issue or Pull request can be pinned
|
||||||
|
func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, error) { |
||||||
|
var maxPin int |
||||||
|
_, err := db.GetEngine(ctx).SQL("SELECT COUNT(pin_order) FROM issue_pin WHERE repo_id = ? AND is_pull = ?", repoID, isPull).Get(&maxPin) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
return maxPin < setting.Repository.Issue.MaxPinned, nil |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_24 //nolint
|
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/models/migrations/base" |
||||||
|
|
||||||
|
"xorm.io/xorm" |
||||||
|
) |
||||||
|
|
||||||
|
func MovePinOrderToTableIssuePin(x *xorm.Engine) error { |
||||||
|
type IssuePin struct { |
||||||
|
ID int64 `xorm:"pk autoincr"` |
||||||
|
RepoID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
IssueID int64 `xorm:"UNIQUE(s) NOT NULL"` |
||||||
|
IsPull bool `xorm:"NOT NULL"` |
||||||
|
PinOrder int `xorm:"DEFAULT 0"` |
||||||
|
} |
||||||
|
|
||||||
|
if err := x.Sync(new(IssuePin)); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := x.Exec("INSERT INTO issue_pin (repo_id, issue_id, is_pull, pin_order) SELECT repo_id, id, is_pull, pin_order FROM issue WHERE pin_order > 0"); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
sess := x.NewSession() |
||||||
|
defer sess.Close() |
||||||
|
return base.DropTableColumns(sess, "issue", "pin_order") |
||||||
|
} |
||||||
Loading…
Reference in new issue