133728e barerepo 1mo
1
package store
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"strings"
133728e barerepo 1mo
8
// Doc is one indexed thing: a file at the tip, a thread, or a repository. Chapter 17.
133728e barerepo 1mo
9
type Doc struct {
133728e barerepo 1mo
10
Repo string
133728e barerepo 1mo
11
Kind string
133728e barerepo 1mo
12
Path string
133728e barerepo 1mo
13
Title string
133728e barerepo 1mo
14
Body string
133728e barerepo 1mo
17
// Doc kinds, ranked in this order, because a barerepo is usually searched for a symbol.
133728e barerepo 1mo
18
const (
133728e barerepo 1mo
19
Code = "code"
133728e barerepo 1mo
20
Thread = "thread"
133728e barerepo 1mo
21
Repository = "repo"
133728e barerepo 1mo
24
// reader marks a name inside the readers column, so john never matches johnson.
133728e barerepo 1mo
25
func reader(name string) string { return "|" + name + "|" }
133728e barerepo 1mo
27
// Readers renders the read set of a repository for the index, per chapter 18's binary read rule.
133728e barerepo 1mo
28
func Readers(owner string, push []string) string {
133728e barerepo 1mo
29
var b strings.Builder
133728e barerepo 1mo
30
b.WriteString(reader(owner))
133728e barerepo 1mo
31
for _, name := range push {
133728e barerepo 1mo
32
if name != owner {
133728e barerepo 1mo
33
b.WriteString(reader(name))
133728e barerepo 1mo
36
return b.String()
133728e barerepo 1mo
39
// PutDocs swaps one repository's documents of one kind, in a transaction, so a query sees one set.
133728e barerepo 1mo
40
func (db *DB) PutDocs(ctx context.Context, repo, kind string, docs []Doc, public bool, readers string) error {
133728e barerepo 1mo
41
tx, err := db.Begin(ctx)
133728e barerepo 1mo
42
if err != nil {
133728e barerepo 1mo
43
return err
133728e barerepo 1mo
45
defer tx.Rollback()
133728e barerepo 1mo
46
if _, err := tx.ExecContext(ctx,
133728e barerepo 1mo
47
`DELETE FROM search_docs WHERE repo = ? AND kind = ?`, repo, kind); err != nil {
133728e barerepo 1mo
48
return err
133728e barerepo 1mo
50
flag := 0
133728e barerepo 1mo
51
if public {
133728e barerepo 1mo
52
flag = 1
133728e barerepo 1mo
54
for _, d := range docs {
133728e barerepo 1mo
55
if _, err := tx.ExecContext(ctx,
133728e barerepo 1mo
56
`INSERT INTO search_docs (repo, kind, path, title, body, public, readers)
133728e barerepo 1mo
57
VALUES (?, ?, ?, ?, ?, ?, ?)`,
133728e barerepo 1mo
58
repo, kind, d.Path, d.Title, d.Body, flag, readers); err != nil {
133728e barerepo 1mo
59
return err
133728e barerepo 1mo
62
return tx.Commit()
133728e barerepo 1mo
65
// SetReadable rewrites who may read a repository's documents, since visibility arrives by push.
133728e barerepo 1mo
66
func (db *DB) SetReadable(ctx context.Context, repo string, public bool, readers string) error {
133728e barerepo 1mo
67
flag := 0
133728e barerepo 1mo
68
if public {
133728e barerepo 1mo
69
flag = 1
133728e barerepo 1mo
71
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
72
`UPDATE search_docs SET public = ?, readers = ? WHERE repo = ?`, flag, readers, repo)
133728e barerepo 1mo
73
return err
133728e barerepo 1mo
76
// EmptyIndex drops every document, so a rebuild produces exactly the index git says it should.
133728e barerepo 1mo
77
func (db *DB) EmptyIndex(ctx context.Context) error {
133728e barerepo 1mo
78
_, err := db.ExecContext(ctx, `DELETE FROM search_docs`)
133728e barerepo 1mo
79
return err
133728e barerepo 1mo
82
// likePattern escapes what LIKE reads as syntax, so a query of 100% finds the string and not everything.
133728e barerepo 1mo
83
func likePattern(query string) string {
133728e barerepo 1mo
84
var b strings.Builder
133728e barerepo 1mo
85
b.WriteByte('%')
133728e barerepo 1mo
86
for _, c := range query {
133728e barerepo 1mo
87
switch c {
133728e barerepo 1mo
88
case '\\', '%', '_':
133728e barerepo 1mo
89
b.WriteByte('\\')
133728e barerepo 1mo
91
b.WriteRune(c)
133728e barerepo 1mo
93
b.WriteByte('%')
133728e barerepo 1mo
94
return b.String()
133728e barerepo 1mo
97
// FindDocs is one query, filtered by read access inside it, because filtering after ranking leaks. 17.
133728e barerepo 1mo
98
func (db *DB) FindDocs(ctx context.Context, viewer, query string, limit int) ([]Doc, error) {
133728e barerepo 1mo
99
pattern := likePattern(strings.ToLower(query))
133728e barerepo 1mo
100
args := []any{pattern, pattern}
133728e barerepo 1mo
101
access := `public = 1`
133728e barerepo 1mo
102
if viewer != "" {
133728e barerepo 1mo
103
access = `(public = 1 OR readers LIKE ?)`
133728e barerepo 1mo
104
args = append(args, "%"+reader(viewer)+"%")
133728e barerepo 1mo
105
}
133728e barerepo 1mo
106
args = append(args, limit)
133728e barerepo 1mo
107
rows, err := db.QueryContext(ctx,
133728e barerepo 1mo
108
`SELECT repo, kind, path, title, body FROM search_docs
133728e barerepo 1mo
109
WHERE (LOWER(body) LIKE ? ESCAPE '\' OR LOWER(title) LIKE ? ESCAPE '\')
133728e barerepo 1mo
110
AND `+access+`
133728e barerepo 1mo
111
ORDER BY CASE kind WHEN 'code' THEN 0 WHEN 'thread' THEN 1 ELSE 2 END, repo, path
133728e barerepo 1mo
112
LIMIT ?`, args...)
133728e barerepo 1mo
113
if err != nil {
133728e barerepo 1mo
114
return nil, err
133728e barerepo 1mo
115
}
133728e barerepo 1mo
116
defer rows.Close()
133728e barerepo 1mo
117
var out []Doc
133728e barerepo 1mo
118
for rows.Next() {
133728e barerepo 1mo
119
var d Doc
133728e barerepo 1mo
120
if err := rows.Scan(&d.Repo, &d.Kind, &d.Path, &d.Title, &d.Body); err != nil {
133728e barerepo 1mo
121
return nil, err
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
out = append(out, d)
133728e barerepo 1mo
124
}
133728e barerepo 1mo
125
return out, rows.Err()
133728e barerepo 1mo
128
// PutDoc writes or replaces one document, which is what an incremental push update needs.
133728e barerepo 1mo
129
func (db *DB) PutDoc(ctx context.Context, d Doc, public bool, readers string) error {
133728e barerepo 1mo
130
flag := 0
133728e barerepo 1mo
131
if public {
133728e barerepo 1mo
132
flag = 1
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
135
`INSERT INTO search_docs (repo, kind, path, title, body, public, readers)
133728e barerepo 1mo
136
VALUES (?, ?, ?, ?, ?, ?, ?)
133728e barerepo 1mo
137
ON CONFLICT (repo, kind, path) DO UPDATE SET
133728e barerepo 1mo
138
title = excluded.title, body = excluded.body,
133728e barerepo 1mo
139
public = excluded.public, readers = excluded.readers`,
133728e barerepo 1mo
140
d.Repo, d.Kind, d.Path, d.Title, d.Body, flag, readers)
133728e barerepo 1mo
141
return err
133728e barerepo 1mo
144
// DeleteDoc drops one document, for a file a push removed.
133728e barerepo 1mo
145
func (db *DB) DeleteDoc(ctx context.Context, repo, kind, path string) error {
133728e barerepo 1mo
146
_, err := db.ExecContext(ctx,
133728e barerepo 1mo
147
`DELETE FROM search_docs WHERE repo = ? AND kind = ? AND path = ?`, repo, kind, path)
133728e barerepo 1mo
148
return err
133728e barerepo 1mo
151
// HasDocs reports whether a repository has been indexed, so a first push does a full pass.
133728e barerepo 1mo
152
func (db *DB) HasDocs(ctx context.Context, repo string) (bool, error) {
133728e barerepo 1mo
153
var n int
133728e barerepo 1mo
154
err := db.QueryRowContext(ctx,
133728e barerepo 1mo
155
`SELECT COUNT(*) FROM search_docs WHERE repo = ?`, repo).Scan(&n)
133728e barerepo 1mo
156
return n > 0, err