mirror of
https://github.com/go-gitea/gitea
synced 2025-08-02 23:58:35 +00:00
Move issue pin to an standalone table for querying performance (#33452)
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>
This commit is contained in:
246
models/issues/issue_pin.go
Normal file
246
models/issues/issue_pin.go
Normal file
@@ -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
|
||||
}
|
Reference in New Issue
Block a user