Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move issue pin to an standalone table for querying performance #33452

Merged
merged 17 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions models/fixtures/issue_pin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-
id: 1
repo_id: 2
issue_id: 4
is_pull: false
pin_order: 1
215 changes: 30 additions & 185 deletions models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ type Issue struct {
// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
Ref string

PinOrder int `xorm:"DEFAULT 0"`
PinOrder int `xorm:"-"` // 0 means not loaded, -1 means loaded but not pinned

DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`

Expand Down Expand Up @@ -290,6 +290,23 @@ func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
return nil
}

func (issue *Issue) LoadPinOrder(ctx context.Context) error {
if issue.PinOrder != 0 {
return nil
}
issuePin, err := GetIssuePin(ctx, issue)
if err != nil && !db.IsErrNotExist(err) {
return err
}

if issuePin != nil {
issue.PinOrder = issuePin.PinOrder
} else {
issue.PinOrder = -1
}
return nil
}

// LoadAttributes loads the attribute of this issue.
func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
if err = issue.LoadRepo(ctx); err != nil {
Expand Down Expand Up @@ -329,6 +346,10 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
return err
}

if err = issue.LoadPinOrder(ctx); err != nil {
return err
}

if err = issue.Comments.LoadAttributes(ctx); err != nil {
return err
}
Expand All @@ -341,6 +362,14 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
return issue.loadReactions(ctx)
}

// IsPinned returns if a Issue is pinned
func (issue *Issue) IsPinned() bool {
if issue.PinOrder == 0 && (!setting.IsProd || setting.IsInTesting) {
log.Fatal("issue's pinorder has not been loaded")
}
return issue.PinOrder > 0
}

func (issue *Issue) ResetAttributesLoaded() {
issue.isLabelsLoaded = false
issue.isMilestoneLoaded = false
Expand Down Expand Up @@ -680,190 +709,6 @@ func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
}

var ErrIssueMaxPinReached = util.NewInvalidArgumentErrorf("the max number of pinned issues has been readched")

// IsPinned returns if a Issue is pinned
func (issue *Issue) IsPinned() bool {
return issue.PinOrder != 0
}

// Pin pins a Issue
func (issue *Issue) Pin(ctx context.Context, user *user_model.User) error {
// If the Issue is already pinned, we don't need to pin it twice
if issue.IsPinned() {
return nil
}

var maxPin int
_, err := db.GetEngine(ctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
if err != nil {
return err
}

// Check if the maximum allowed Pins reached
if maxPin >= setting.Repository.Issue.MaxPinned {
return ErrIssueMaxPinReached
}

_, err = db.GetEngine(ctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]any{
"pin_order": maxPin + 1,
})
if err != nil {
return err
}

// Add the pin event to the history
opts := &CreateCommentOptions{
Type: CommentTypePin,
Doer: user,
Repo: issue.Repo,
Issue: issue,
}
if _, err = CreateComment(ctx, opts); err != nil {
return err
}

return nil
}

// UnpinIssue unpins a Issue
func (issue *Issue) Unpin(ctx context.Context, user *user_model.User) error {
// If the Issue is not pinned, we don't need to unpin it
if !issue.IsPinned() {
return nil
}

// This sets the Pin for all Issues that come after the unpined Issue to the correct value
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
if err != nil {
return err
}

_, err = db.GetEngine(ctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]any{
"pin_order": 0,
})
if err != nil {
return err
}

// Add the unpin event to the history
opts := &CreateCommentOptions{
Type: CommentTypeUnpin,
Doer: user,
Repo: issue.Repo,
Issue: issue,
}
if _, err = CreateComment(ctx, opts); err != nil {
return err
}

return nil
}

// PinOrUnpin pins or unpins a Issue
func (issue *Issue) PinOrUnpin(ctx context.Context, user *user_model.User) error {
if !issue.IsPinned() {
return issue.Pin(ctx, user)
}

return issue.Unpin(ctx, user)
}

// MovePin moves a Pinned Issue to a new Position
func (issue *Issue) MovePin(ctx context.Context, newPosition int) error {
// If the Issue is not pinned, we can't move them
if !issue.IsPinned() {
return nil
}

if newPosition < 1 {
return fmt.Errorf("The Position can't be lower than 1")
}

dbctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()

var maxPin int
_, err = db.GetEngine(dbctx).SQL("SELECT MAX(pin_order) FROM issue WHERE repo_id = ? AND is_pull = ?", issue.RepoID, issue.IsPull).Get(&maxPin)
if err != nil {
return err
}

// If the new Position bigger than the current Maximum, set it to the Maximum
if newPosition > maxPin+1 {
newPosition = maxPin + 1
}

// Lower the Position of all Pinned Issue that came after the current Position
_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order - 1 WHERE repo_id = ? AND is_pull = ? AND pin_order > ?", issue.RepoID, issue.IsPull, issue.PinOrder)
if err != nil {
return err
}

// Higher the Position of all Pinned Issues that comes after the new Position
_, err = db.GetEngine(dbctx).Exec("UPDATE issue SET pin_order = pin_order + 1 WHERE repo_id = ? AND is_pull = ? AND pin_order >= ?", issue.RepoID, issue.IsPull, newPosition)
if err != nil {
return err
}

_, err = db.GetEngine(dbctx).Table("issue").
Where("id = ?", issue.ID).
Update(map[string]any{
"pin_order": newPosition,
})
if err != nil {
return err
}

return committer.Commit()
}

// GetPinnedIssues returns the pinned Issues for the given Repo and type
func GetPinnedIssues(ctx context.Context, repoID int64, isPull bool) (IssueList, error) {
issues := make(IssueList, 0)

err := db.GetEngine(ctx).
Table("issue").
Where("repo_id = ?", repoID).
And("is_pull = ?", isPull).
And("pin_order > 0").
OrderBy("pin_order").
Find(&issues)
if err != nil {
return nil, err
}

err = issues.LoadAttributes(ctx)
if 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 WHERE repo_id = ? AND is_pull = ? AND pin_order > 0", repoID, isPull).Get(&maxPin)
if err != nil {
return false, err
}

return maxPin < setting.Repository.Issue.MaxPinned, nil
}

// IsErrIssueMaxPinReached returns if the error is, that the User can't pin more Issues
func IsErrIssueMaxPinReached(err error) bool {
return err == ErrIssueMaxPinReached
}

// InsertIssues insert issues to database
func InsertIssues(ctx context.Context, issues ...*Issue) error {
ctx, committer, err := db.TxContext(ctx)
Expand Down
33 changes: 33 additions & 0 deletions models/issues/issue_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,39 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
return nil
}

func (issues IssueList) LoadPinOrder(ctx context.Context) error {
if len(issues) == 0 {
return nil
}

issueIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
return issue.ID, issue.PinOrder == 0
})
if len(issueIDs) == 0 {
return nil
}
issuePins, err := GetIssuePinsByIssueIDs(ctx, issueIDs)
if err != nil {
return err
}

for _, issue := range issues {
if issue.PinOrder != 0 {
continue
}
for _, pin := range issuePins {
if pin.IssueID == issue.ID {
issue.PinOrder = pin.PinOrder
break
}
}
if issue.PinOrder == 0 {
issue.PinOrder = -1
}
}
return nil
}

// loadAttributes loads all attributes, expect for attachments and comments
func (issues IssueList) LoadAttributes(ctx context.Context) error {
if _, err := issues.LoadRepositories(ctx); err != nil {
Expand Down
Loading
Loading