package store import ( "context" "strings" ) // Doc is one indexed thing: a file at the tip, a thread, or a repository. Chapter 17. type Doc struct { Repo string Kind string Path string Title string Body string } // Doc kinds, ranked in this order, because a barerepo is usually searched for a symbol. const ( Code = "code" Thread = "thread" Repository = "repo" ) // reader marks a name inside the readers column, so john never matches johnson. func reader(name string) string { return "|" + name + "|" } // Readers renders the read set of a repository for the index, per chapter 18's binary read rule. func Readers(owner string, push []string) string { var b strings.Builder b.WriteString(reader(owner)) for _, name := range push { if name != owner { b.WriteString(reader(name)) } } return b.String() } // PutDocs swaps one repository's documents of one kind, in a transaction, so a query sees one set. func (db *DB) PutDocs(ctx context.Context, repo, kind string, docs []Doc, public bool, readers string) error { tx, err := db.Begin(ctx) if err != nil { return err } defer tx.Rollback() if _, err := tx.ExecContext(ctx, `DELETE FROM search_docs WHERE repo = ? AND kind = ?`, repo, kind); err != nil { return err } flag := 0 if public { flag = 1 } for _, d := range docs { if _, err := tx.ExecContext(ctx, `INSERT INTO search_docs (repo, kind, path, title, body, public, readers) VALUES (?, ?, ?, ?, ?, ?, ?)`, repo, kind, d.Path, d.Title, d.Body, flag, readers); err != nil { return err } } return tx.Commit() } // SetReadable rewrites who may read a repository's documents, since visibility arrives by push. func (db *DB) SetReadable(ctx context.Context, repo string, public bool, readers string) error { flag := 0 if public { flag = 1 } _, err := db.ExecContext(ctx, `UPDATE search_docs SET public = ?, readers = ? WHERE repo = ?`, flag, readers, repo) return err } // EmptyIndex drops every document, so a rebuild produces exactly the index git says it should. func (db *DB) EmptyIndex(ctx context.Context) error { _, err := db.ExecContext(ctx, `DELETE FROM search_docs`) return err } // likePattern escapes what LIKE reads as syntax, so a query of 100% finds the string and not everything. func likePattern(query string) string { var b strings.Builder b.WriteByte('%') for _, c := range query { switch c { case '\\', '%', '_': b.WriteByte('\\') } b.WriteRune(c) } b.WriteByte('%') return b.String() } // FindDocs is one query, filtered by read access inside it, because filtering after ranking leaks. 17. func (db *DB) FindDocs(ctx context.Context, viewer, query string, limit int) ([]Doc, error) { pattern := likePattern(strings.ToLower(query)) args := []any{pattern, pattern} access := `public = 1` if viewer != "" { access = `(public = 1 OR readers LIKE ?)` args = append(args, "%"+reader(viewer)+"%") } args = append(args, limit) rows, err := db.QueryContext(ctx, `SELECT repo, kind, path, title, body FROM search_docs WHERE (LOWER(body) LIKE ? ESCAPE '\' OR LOWER(title) LIKE ? ESCAPE '\') AND `+access+` ORDER BY CASE kind WHEN 'code' THEN 0 WHEN 'thread' THEN 1 ELSE 2 END, repo, path LIMIT ?`, args...) if err != nil { return nil, err } defer rows.Close() var out []Doc for rows.Next() { var d Doc if err := rows.Scan(&d.Repo, &d.Kind, &d.Path, &d.Title, &d.Body); err != nil { return nil, err } out = append(out, d) } return out, rows.Err() } // PutDoc writes or replaces one document, which is what an incremental push update needs. func (db *DB) PutDoc(ctx context.Context, d Doc, public bool, readers string) error { flag := 0 if public { flag = 1 } _, err := db.ExecContext(ctx, `INSERT INTO search_docs (repo, kind, path, title, body, public, readers) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (repo, kind, path) DO UPDATE SET title = excluded.title, body = excluded.body, public = excluded.public, readers = excluded.readers`, d.Repo, d.Kind, d.Path, d.Title, d.Body, flag, readers) return err } // DeleteDoc drops one document, for a file a push removed. func (db *DB) DeleteDoc(ctx context.Context, repo, kind, path string) error { _, err := db.ExecContext(ctx, `DELETE FROM search_docs WHERE repo = ? AND kind = ? AND path = ?`, repo, kind, path) return err } // HasDocs reports whether a repository has been indexed, so a first push does a full pass. func (db *DB) HasDocs(ctx context.Context, repo string) (bool, error) { var n int err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM search_docs WHERE repo = ?`, repo).Scan(&n) return n > 0, err }