Add files via upload

This commit is contained in:
公明
2026-07-01 14:29:58 +08:00
committed by GitHub
parent 9964e13197
commit 930eb47013
2 changed files with 115 additions and 17 deletions
+33 -17
View File
@@ -111,19 +111,43 @@ func (db *DB) GetProject(id string) (*Project, error) {
return &p, nil
}
// CountProjects 统计项目数量。
func (db *DB) CountProjects(status, search string) (int, error) {
query := `SELECT COUNT(*) FROM projects WHERE 1=1`
args := []interface{}{}
func projectListSearchPattern(q string) string {
q = strings.TrimSpace(q)
if q == "" {
return ""
}
var b strings.Builder
b.WriteByte('%')
for _, r := range q {
switch r {
case '%', '_', '\\':
b.WriteByte('\\')
b.WriteRune(r)
default:
b.WriteRune(r)
}
}
b.WriteByte('%')
return b.String()
}
func appendProjectListFilters(query string, args []interface{}, status, search string) (string, []interface{}) {
if s := strings.TrimSpace(status); s != "" {
query += " AND status = ?"
args = append(args, s)
}
if q := strings.TrimSpace(search); q != "" {
pattern := "%" + q + "%"
query += " AND (name LIKE ? OR COALESCE(description,'') LIKE ?)"
args = append(args, pattern, pattern)
if pattern := projectListSearchPattern(search); pattern != "" {
query += ` AND (LOWER(name) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(description,'')) LIKE LOWER(?) ESCAPE '\' OR LOWER(id) LIKE LOWER(?) ESCAPE '\')`
args = append(args, pattern, pattern, pattern)
}
return query, args
}
// CountProjects 统计项目数量。
func (db *DB) CountProjects(status, search string) (int, error) {
query := `SELECT COUNT(*) FROM projects WHERE 1=1`
args := []interface{}{}
query, args = appendProjectListFilters(query, args, status, search)
var count int
if err := db.QueryRow(query, args...).Scan(&count); err != nil {
return 0, fmt.Errorf("统计项目失败: %w", err)
@@ -139,15 +163,7 @@ func (db *DB) ListProjects(status, search string, limit, offset int) ([]*Project
query := `SELECT id, name, COALESCE(description,''), COALESCE(scope_json,''), status, pinned, created_at, updated_at
FROM projects WHERE 1=1`
args := []interface{}{}
if s := strings.TrimSpace(status); s != "" {
query += " AND status = ?"
args = append(args, s)
}
if q := strings.TrimSpace(search); q != "" {
pattern := "%" + q + "%"
query += " AND (name LIKE ? OR COALESCE(description,'') LIKE ?)"
args = append(args, pattern, pattern)
}
query, args = appendProjectListFilters(query, args, status, search)
query += " ORDER BY pinned DESC, updated_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
+82
View File
@@ -0,0 +1,82 @@
package database
import (
"path/filepath"
"testing"
"go.uber.org/zap"
)
func TestListProjectsSearchCaseInsensitive(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "projects-search.db")
db, err := NewDB(dbPath, zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
p1, err := db.CreateProject(&Project{Name: "Alpha Security Review", Status: "active"})
if err != nil {
t.Fatal(err)
}
p2, err := db.CreateProject(&Project{Name: "beta-scan", Status: "active"})
if err != nil {
t.Fatal(err)
}
if _, err := db.CreateProject(&Project{Name: "Other", Status: "archived"}); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
search string
status string
want []string
}{
{name: "case insensitive name", search: "alpha", status: "active", want: []string{p1.ID}},
{name: "upper query", search: "BETA", status: "active", want: []string{p2.ID}},
{name: "search by id substring", search: p1.ID[:8], status: "", want: []string{p1.ID}},
{name: "status filter", search: "alpha", status: "archived", want: nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
list, err := db.ListProjects(tc.status, tc.search, 50, 0)
if err != nil {
t.Fatal(err)
}
got := make([]string, 0, len(list))
for _, p := range list {
got = append(got, p.ID)
}
if len(got) != len(tc.want) {
t.Fatalf("got %v want %v", got, tc.want)
}
for i := range got {
if got[i] != tc.want[i] {
t.Fatalf("got %v want %v", got, tc.want)
}
}
})
}
}
func TestProjectListSearchPatternEscapesWildcards(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "projects-like.db")
db, err := NewDB(dbPath, zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
p, err := db.CreateProject(&Project{Name: "100% coverage", Status: "active"})
if err != nil {
t.Fatal(err)
}
list, err := db.ListProjects("active", "100%", 50, 0)
if err != nil {
t.Fatal(err)
}
if len(list) != 1 || list[0].ID != p.ID {
t.Fatalf("expected exact match for literal %% query, got %#v", list)
}
}