internal/store/search.go

5e9732fb1d5142142fdae2985f07125cf852647c
gitbay/internal/store/search.go history · blame · raw

79 lines · 2662 bytes

 1package store
 2
 3// Search across repositories, for the `search` command and the web's
 4// /search page. Visibility is the same rule everywhere: public plus what
 5// the user reaches, so an anonymous caller (user id 0) sees only public
 6// rows rather than a different query.
 7
 8// visibleCond admits public repositories and anything the user reaches.
 9const visibleCond = `(r.visibility = 'public' OR ` + reachableCond + `)`
10
11// VisibleRepos returns every repository the user may read, public ones
12// included, ordered by owner then name.
13func (s *Store) VisibleRepos(userID int64) ([]Repo, error) {
14	rows, err := s.DB.Query(repoSelect+" WHERE "+visibleCond+" ORDER BY 4, r.name", userID)
15	if err != nil {
16		return nil, err
17	}
18	defer rows.Close()
19	var out []Repo
20	for rows.Next() {
21		r, err := scanRepo(rows)
22		if err != nil {
23			return nil, err
24		}
25		out = append(out, r)
26	}
27	return out, rows.Err()
28}
29
30// SearchIssues returns issues whose title contains q, newest activity
31// first. Titles only: body search is FTS work that belongs with the
32// per-repository issue search.
33func (s *Store) SearchIssues(userID int64, q string, limit int) ([]DashboardItem, error) {
34	return s.searchQuery("issues", userID, q, limit)
35}
36
37// SearchMRs is SearchIssues for merge requests.
38func (s *Store) SearchMRs(userID int64, q string, limit int) ([]DashboardItem, error) {
39	return s.searchQuery("merge_requests", userID, q, limit)
40}
41
42func (s *Store) searchQuery(table string, userID int64, q string, limit int) ([]DashboardItem, error) {
43	rows, err := s.DB.Query(`
44		SELECT COALESCE(u.username, o.name) || '/' || r.name,
45		       x.number, x.title, au.username, x.state, x.updated_at
46		FROM `+table+` x
47		JOIN repos r ON r.id = x.repo_id
48		LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
49		LEFT JOIN orgs o  ON r.owner_kind = 'org'  AND o.id = r.owner_id
50		JOIN users au ON au.id = x.author_id
51		WHERE x.title LIKE '%' || ?2 || '%' ESCAPE '\' AND `+visibleCond+`
52		ORDER BY x.updated_at DESC LIMIT ?3`, userID, escapeLike(q), limit)
53	if err != nil {
54		return nil, err
55	}
56	defer rows.Close()
57	var out []DashboardItem
58	for rows.Next() {
59		var d DashboardItem
60		if err := rows.Scan(&d.RepoPath, &d.Number, &d.Title, &d.Author, &d.State, &d.UpdatedAt); err != nil {
61			return nil, err
62		}
63		out = append(out, d)
64	}
65	return out, rows.Err()
66}
67
68// escapeLike neutralises the LIKE wildcards, so a query containing % or _
69// matches those characters rather than everything.
70func escapeLike(q string) string {
71	var b []byte
72	for i := 0; i < len(q); i++ {
73		if c := q[i]; c == '%' || c == '_' || c == '\\' {
74			b = append(b, '\\')
75		}
76		b = append(b, q[i])
77	}
78	return string(b)
79}