mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	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>
		
			
				
	
	
		
			247 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			247 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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
 | 
						|
}
 |