mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
144 lines
4.3 KiB
Go
144 lines
4.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
issue_model "github.com/hanzoai/git/models/issues"
|
|
"github.com/hanzoai/git/modules/indexer"
|
|
indexer_internal "github.com/hanzoai/git/modules/indexer/internal"
|
|
inner_db "github.com/hanzoai/git/modules/indexer/internal/db"
|
|
"github.com/hanzoai/git/modules/indexer/issues/internal"
|
|
"github.com/hanzoai/git/modules/util"
|
|
|
|
"github.com/hanzoai/builder"
|
|
)
|
|
|
|
var _ internal.Indexer = (*Indexer)(nil)
|
|
|
|
// Indexer implements Indexer interface to use database's like search
|
|
type Indexer struct {
|
|
indexer_internal.Indexer
|
|
}
|
|
|
|
func (i *Indexer) SupportedSearchModes() []indexer.SearchMode {
|
|
return indexer.SearchModesExactWords()
|
|
}
|
|
|
|
var GetIndexer = sync.OnceValue(func() *Indexer {
|
|
return &Indexer{Indexer: &inner_db.Indexer{}}
|
|
})
|
|
|
|
// Index dummy function
|
|
func (i *Indexer) Index(_ context.Context, _ ...*internal.IndexerData) error {
|
|
return nil
|
|
}
|
|
|
|
// Delete dummy function
|
|
func (i *Indexer) Delete(_ context.Context, _ ...int64) error {
|
|
return nil
|
|
}
|
|
|
|
func buildMatchQuery(mode indexer.SearchModeType, colName, keyword string) builder.Cond {
|
|
if mode == indexer.SearchModeExact {
|
|
return db.BuildCaseInsensitiveLike(colName, keyword)
|
|
}
|
|
|
|
// match words
|
|
cond := builder.NewCond()
|
|
fields := strings.Fields(keyword)
|
|
if len(fields) == 0 {
|
|
return builder.Expr("1=1")
|
|
}
|
|
for _, field := range fields {
|
|
if field == "" {
|
|
continue
|
|
}
|
|
cond = cond.And(db.BuildCaseInsensitiveLike(colName, field))
|
|
}
|
|
return cond
|
|
}
|
|
|
|
// Search searches for issues
|
|
func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
|
|
// FIXME: I tried to avoid importing models here, but it seems to be impossible.
|
|
// We can provide a function to register the search function, so models/issues can register it.
|
|
// So models/issues will import modules/indexer/issues, it's OK because it's by design.
|
|
// But modules/indexer/issues has already imported models/issues to do UpdateRepoIndexer and UpdateIssueIndexer.
|
|
// And to avoid circular import, we have to move the functions to another package.
|
|
// I believe it should be services/indexer, sounds great!
|
|
// But the two functions are used in modules/notification/indexer, that means we will import services/indexer in modules/notification/indexer.
|
|
// So that's the root problem:
|
|
// The notification is defined in modules, but it's using lots of things should be in services.
|
|
|
|
cond := builder.NewCond()
|
|
|
|
if options.Keyword != "" {
|
|
repoCond := builder.In("repo_id", options.RepoIDs)
|
|
if len(options.RepoIDs) == 1 {
|
|
repoCond = builder.Eq{"repo_id": options.RepoIDs[0]}
|
|
}
|
|
subQuery := builder.Select("id").From("issue").Where(repoCond)
|
|
searchMode := util.IfZero(options.SearchMode, i.SupportedSearchModes()[0].ModeValue)
|
|
cond = builder.Or(
|
|
buildMatchQuery(searchMode, "issue.name", options.Keyword),
|
|
buildMatchQuery(searchMode, "issue.content", options.Keyword),
|
|
builder.In("issue.id", builder.Select("issue_id").
|
|
From("comment").
|
|
Where(builder.And(
|
|
builder.Eq{"type": issue_model.CommentTypeComment},
|
|
builder.In("issue_id", subQuery),
|
|
buildMatchQuery(searchMode, "content", options.Keyword),
|
|
)),
|
|
),
|
|
)
|
|
|
|
if options.IsKeywordNumeric() {
|
|
cond = cond.Or(
|
|
builder.Eq{"`index`": options.Keyword},
|
|
)
|
|
}
|
|
}
|
|
|
|
opt, err := ToDBOptions(ctx, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If pagesize == 0, return total count only. It's a special case for search count.
|
|
if options.Paginator != nil && options.Paginator.PageSize == 0 {
|
|
total, err := issue_model.CountIssues(ctx, opt, cond)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &internal.SearchResult{
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
return i.FindWithIssueOptions(ctx, opt, cond)
|
|
}
|
|
|
|
func (i *Indexer) FindWithIssueOptions(ctx context.Context, opt *issue_model.IssuesOptions, otherConds ...builder.Cond) (*internal.SearchResult, error) {
|
|
ids, total, err := issue_model.IssueIDs(ctx, opt, otherConds...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hits := make([]internal.Match, 0, len(ids))
|
|
for _, id := range ids {
|
|
hits = append(hits, internal.Match{
|
|
ID: id,
|
|
})
|
|
}
|
|
return &internal.SearchResult{
|
|
Total: total,
|
|
Hits: hits,
|
|
}, nil
|
|
}
|