package index
import (
"sort"
"strings"
"testing"
)
// #263: a tip move rewrites only the files that changed. Whatever it
// skips, the rows and the full-text index read exactly as if the tree
// had been written from scratch: an unchanged file is found at the new
// tip, a changed one by its new words and not its old, a new one is
// found, a removed one is not, and the FTS index is consistent.
func TestATreeMoveRewritesOnlyWhatChanged(t *testing.T) {
ix := openIndex(t)
if err := ix.ReplaceTree("v1", []TreeText{
{Path: "keep.go", Body: "steady alpaca"},
{Path: "edit.go", Body: "old wombat"},
{Path: "gone.go", Body: "doomed narwhal"},
{Path: "README.md", Body: "intro", Doc: true},
}); err != nil {
t.Fatal(err)
}
if err := ix.ReplaceTree("v2", []TreeText{
{Path: "keep.go", Body: "steady alpaca"},
{Path: "edit.go", Body: "new quokka"},
{Path: "added.go", Body: "fresh okapi"},
{Path: "README.md", Body: "intro", Doc: true},
}); err != nil {
t.Fatal(err)
}
refs := func(q string) []string {
t.Helper()
rs, err := ix.Search(q, "", 20)
if err != nil {
t.Fatal(err)
}
var out []string
for _, r := range rs {
out = append(out, r.Ref)
}
sort.Strings(out)
return out
}
for q, want := range map[string]string{
"alpaca": "v2/keep.go", "quokka": "v2/edit.go", "okapi": "v2/added.go", "intro": "v2/README.md",
"wombat": "", "narwhal": "",
} {
got := strings.Join(refs(q), " ")
if got != want {
t.Errorf("FAIL: %q finds %q, want %q", q, got, want)
}
}
if _, err := ix.db.Exec(`INSERT INTO search_fts (search_fts) VALUES ('integrity-check')`); err != nil {
t.Errorf("FAIL: the full-text index disagrees with its rows: %v", err)
}
var trig string
ix.db.QueryRow(`SELECT sql FROM sqlite_master WHERE name = 'search_doc_au'`).Scan(&trig)
if !strings.Contains(trig, "UPDATE OF text") {
t.Errorf("FAIL: the update trigger still fires on any column: %s", trig)
}
}
// An index made before the narrowed trigger has it replaced on open.
func TestAnOldSearchUpdateTriggerIsReplacedOnOpen(t *testing.T) {
path := t.TempDir() + "/index.db"
ix, err := Open(path)
if err != nil {
t.Fatal(err)
}
if _, err := ix.w.Exec(`DROP TRIGGER search_doc_au;
CREATE TRIGGER search_doc_au AFTER UPDATE ON search_doc BEGIN
INSERT INTO search_fts (search_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
INSERT INTO search_fts (rowid, text) VALUES (new.rowid, new.text);
END;`); err != nil {
t.Fatal(err)
}
ix.Close()
ix, err = Open(path)
if err != nil {
t.Fatal(err)
}
defer ix.Close()
var trig string
ix.db.QueryRow(`SELECT sql FROM sqlite_master WHERE name = 'search_doc_au'`).Scan(&trig)
if !strings.Contains(trig, "UPDATE OF text") {
t.Errorf("FAIL: reopening kept the old trigger: %s", trig)
}
}
// A tip move compares stored digests, not text: an identical tree writes
// nothing, and a row written before digests existed is rewritten once
// and carries one after.
func TestATreeMoveComparesDigests(t *testing.T) {
ix := openIndex(t)
tree := []TreeText{{Path: "a.go", Body: "alpha"}, {Path: "doc.md", Body: "beta", Doc: true}}
if err := ix.ReplaceTree("v1", tree); err != nil {
t.Fatal(err)
}
state := func() string {
rows, err := ix.db.Query(`SELECT hash, rowid, digest FROM search_doc WHERE kind IN ('doc','file') ORDER BY hash`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
var out []string
for rows.Next() {
var h, d string
var id int64
rows.Scan(&h, &id, &d)
if d == "" {
t.Errorf("FAIL: %s has no digest", h)
}
out = append(out, h+"@"+strings.Repeat("|", int(id%7))+d)
}
return strings.Join(out, " ")
}
before := state()
if err := ix.ReplaceTree("v2", tree); err != nil {
t.Fatal(err)
}
if after := state(); after != before {
t.Errorf("FAIL: an identical tree rewrote rows:\n%s\n%s", before, after)
}
// A row from before digests: rewritten once, then it has one.
if _, err := ix.w.Exec(`UPDATE search_doc SET digest = '' WHERE hash = 'file:a.go'`); err != nil {
t.Fatal(err)
}
if err := ix.ReplaceTree("v3", tree); err != nil {
t.Fatal(err)
}
state()
if rs, _ := ix.Search("alpha", "", 5); len(rs) != 1 || rs[0].Ref != "v3/a.go" {
t.Errorf("FAIL: after the moves alpha finds %+v", rs)
}
}