mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +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
 | |
| }
 |