package index
import (
"fmt"
"slices"
"strings"
"testing"
)
// A chip that stands for several kinds ("l" is a landing and its
// comments, "code" is check-ins, landings and technotes) must walk the
// kind index in date order and stop at the page, as a single kind does.
// As one IN list it read every matching row and sorted them in a temp
// b-tree: 83ms on tcl for a page of sixty, over a second across the
// dashboard's repositories.
func TestCompositeKindTimelineDoesNotSort(t *testing.T) {
ix := openIndex(t)
for _, f := range []TimelineFilter{
{Limit: 60, Kind: "code"},
{Limit: 60, Kind: "l"},
{Limit: 60, Kind: "code", Before: "2026-01-01T00:00:00~abc"},
{Limit: 60, Kind: "code", User: "dan"},
} {
q, args, ok, err := ix.timelineQuery(f)
if err != nil || !ok {
t.Fatalf("%+v: ok=%v err=%v", f, ok, err)
}
plan := queryPlan(t, ix, q, args...)
if strings.Contains(plan, "TEMP B-TREE") {
t.Errorf("%+v sorts every matching event in a temp b-tree:\n%s", f, plan)
}
if !strings.Contains(plan, "event_by_kind") {
t.Errorf("%+v does not walk event_by_kind:\n%s", f, plan)
}
}
}
// Whatever shape the query takes, a composite chip's pages are its
// kinds merged in the timeline's one total order, (date, hash)
// descending, and paging by cursor visits each event once.
func TestCompositeKindTimelineIsItsKindsMerged(t *testing.T) {
ix := openIndex(t)
kinds := []string{"ci", "l", "lc", "e", "t", "w", "f"}
type row struct{ hash, kind, date, user string }
var rows []row
for i := 0; i < 70; i++ {
// Every third pair shares a timestamp, so the hash tiebreak is
// exercised across kinds.
r := row{
hash: fmt.Sprintf("h%03d", i),
kind: kinds[i%len(kinds)],
date: fmt.Sprintf("2026-01-%02dT00:00:%02d", 1+i/6, (i/2)%60),
user: []string{"dan", "ann"}[i%2],
}
rows = append(rows, r)
if _, err := ix.w.Exec(`INSERT INTO event (hash, kind, ref, date, user, comment) VALUES (?, ?, ?, ?, ?, '')`,
r.hash, r.kind, r.hash, r.date, r.user); err != nil {
t.Fatal(err)
}
}
want := func(set []string, user string) []string {
var got []row
for _, r := range rows {
if slices.Contains(set, r.kind) && (user == "" || r.user == user) {
got = append(got, r)
}
}
slices.SortFunc(got, func(a, b row) int {
if c := strings.Compare(b.date, a.date); c != 0 {
return c
}
return strings.Compare(b.hash, a.hash)
})
var out []string
for _, r := range got {
out = append(out, r.hash)
}
return out
}
for _, c := range []struct {
kind, user string
set []string
}{
{"code", "", []string{"ci", "l", "lc", "e"}},
{"code", "dan", []string{"ci", "l", "lc", "e"}},
{"l", "", []string{"l", "lc"}},
} {
var got []string
before := ""
for page := 0; page < 20; page++ {
entries, more, err := ix.Timeline(TimelineFilter{Limit: 4, Kind: c.kind, User: c.user, Before: before})
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
got = append(got, e.Hash)
}
if !more {
break
}
last := entries[len(entries)-1]
before = last.Date + "~" + last.Hash
}
if exp := want(c.set, c.user); !slices.Equal(got, exp) {
t.Errorf("kind=%s user=%q paged\n got %v\nwant %v", c.kind, c.user, got, exp)
}
}
}