package browse
import (
"context"
"errors"
"testing"
)
func TestBranches(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
branches, err := db.Branches(ctx)
if err != nil {
t.Fatal(err)
}
if len(branches) != 2 {
t.Fatalf("want 2 branches, got %d: %+v", len(branches), branches)
}
// main sorts first (default).
if branches[0].Name != "main" {
t.Errorf("want main first, got %q", branches[0].Name)
}
names := map[string]string{}
for _, b := range branches {
names[b.Name] = b.Head
}
if _, ok := names["dev"]; !ok {
t.Errorf("missing dev branch: %+v", branches)
}
// main head is C4 (add items); dev head is C2 (insert users): distinct.
if names["main"] == names["dev"] {
t.Errorf("main and dev unexpectedly share a head %q", names["main"])
}
if got, want := names["main"], commitHash(t, msgAddItem); got != want {
t.Errorf("main head = %q, want C4 %q", got, want)
}
if got, want := names["dev"], commitHash(t, msgInsert); got != want {
t.Errorf("dev head = %q, want C2 %q", got, want)
}
if got := DefaultBranch(branches); got != "main" {
t.Errorf("DefaultBranch = %q, want main", got)
}
}
func TestLogTopoOrder(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
commits, next, err := db.Log(ctx, "main", "", 100)
if err != nil {
t.Fatal(err)
}
if next != "" {
t.Errorf("full log should have no next page, got %q", next)
}
wantOrder := []string{msgAddItem, msgModify, msgInsert, msgAddUser, msgInitial}
if len(commits) != len(wantOrder) {
t.Fatalf("want %d commits, got %d", len(wantOrder), len(commits))
}
for i, want := range wantOrder {
if commits[i].Message != want {
t.Errorf("commit[%d] message = %q, want %q", i, commits[i].Message, want)
}
}
// Author/email come from the CLI global config for the content commits.
if commits[0].Author != "CLI Fixture" || commits[0].Email != "cli@fixture.test" {
t.Errorf("head author/email = %q/%q", commits[0].Author, commits[0].Email)
}
// Parent linkage: each commit's first parent is the next-older commit;
// the initial commit has no parent.
for i := 0; i < len(commits)-1; i++ {
if len(commits[i].ParentHashes) == 0 {
t.Fatalf("commit[%d] %q unexpectedly has no parent", i, commits[i].Message)
}
if commits[i].ParentHashes[0] != commits[i+1].Hash {
t.Errorf("commit[%d] parent = %q, want %q", i, commits[i].ParentHashes[0], commits[i+1].Hash)
}
}
if last := commits[len(commits)-1]; len(last.ParentHashes) != 0 {
t.Errorf("initial commit should have no parents, got %v", last.ParentHashes)
}
if commits[len(commits)-1].Date.IsZero() {
t.Errorf("initial commit date should be set")
}
}
func TestLogPagination(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// Page through the full history two commits at a time.
var got []string
from := ""
pages := 0
for {
commits, next, err := db.Log(ctx, "main", from, 2)
if err != nil {
t.Fatal(err)
}
if len(commits) == 0 {
t.Fatalf("empty page (from=%q)", from)
}
if len(commits) > 2 {
t.Fatalf("page larger than limit: %d", len(commits))
}
for _, c := range commits {
got = append(got, c.Message)
}
pages++
if pages > 10 {
t.Fatal("pagination did not terminate")
}
if next == "" {
break
}
from = next
}
want := []string{msgAddItem, msgModify, msgInsert, msgAddUser, msgInitial}
if len(got) != len(want) {
t.Fatalf("paged %d commits, want %d: %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("paged[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestLogFromHash(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// Start the walk from C2 (insert users): should yield C2, C1, C0.
commits, next, err := db.Log(ctx, "main", commitHash(t, msgInsert), 100)
if err != nil {
t.Fatal(err)
}
if next != "" {
t.Errorf("want no next page, got %q", next)
}
want := []string{msgInsert, msgAddUser, msgInitial}
if len(commits) != len(want) {
t.Fatalf("want %d commits from C2, got %d", len(want), len(commits))
}
for i, w := range want {
if commits[i].Message != w {
t.Errorf("from-hash commit[%d] = %q, want %q", i, commits[i].Message, w)
}
}
}
func TestLogInvalidLimit(t *testing.T) {
db := openFixture(t)
if _, _, err := db.Log(context.Background(), "main", "", 0); err == nil {
t.Fatal("expected error for non-positive limit")
}
}
func TestTables(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
tables, err := db.Tables(ctx, "main")
if err != nil {
t.Fatal(err)
}
byName := map[string]TableInfo{}
for _, ti := range tables {
byName[ti.Name] = ti
}
users, ok := byName["users"]
if !ok {
t.Fatalf("missing users table: %+v", tables)
}
if users.RowCount != 4 {
t.Errorf("users row count = %d, want 4", users.RowCount)
}
if len(users.Columns) != 2 {
t.Fatalf("users columns = %d, want 2", len(users.Columns))
}
id := users.Columns[0]
if id.Name != "id" || !id.PrimaryKey || id.Nullable {
t.Errorf("users.id column = %+v", id)
}
if id.Type != "int" {
t.Errorf("users.id type = %q, want int", id.Type)
}
name := users.Columns[1]
if name.Name != "name" || name.PrimaryKey || !name.Nullable {
t.Errorf("users.name column = %+v", name)
}
if name.Type != "varchar(64)" {
t.Errorf("users.name type = %q, want varchar(64)", name.Type)
}
items, ok := byName["items"]
if !ok {
t.Fatalf("missing items table: %+v", tables)
}
if items.RowCount != 0 {
t.Errorf("items row count = %d, want 0", items.RowCount)
}
}
func TestRowsPagination(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
first, err := db.Rows(ctx, "main", "users", 0, 2)
if err != nil {
t.Fatal(err)
}
if first.Total != 4 {
t.Errorf("total = %d, want 4", first.Total)
}
if first.Offset != 0 {
t.Errorf("offset = %d, want 0", first.Offset)
}
wantCols := []string{"id", "name"}
if len(first.Columns) != 2 || first.Columns[0] != wantCols[0] || first.Columns[1] != wantCols[1] {
t.Errorf("columns = %v, want %v", first.Columns, wantCols)
}
// Ordered by primary key: rows 1,2. id=1 was renamed to alicia in C3.
wantFirst := [][]string{{"1", "alicia"}, {"2", "bob"}}
if !equalRows(first.Rows, wantFirst) {
t.Errorf("page 1 rows = %v, want %v", first.Rows, wantFirst)
}
second, err := db.Rows(ctx, "main", "users", 2, 2)
if err != nil {
t.Fatal(err)
}
wantSecond := [][]string{{"3", "carol"}, {"4", "dave"}}
if !equalRows(second.Rows, wantSecond) {
t.Errorf("page 2 rows = %v, want %v", second.Rows, wantSecond)
}
// Offset at/after the end yields an empty page, not an error.
past, err := db.Rows(ctx, "main", "users", 4, 2)
if err != nil {
t.Fatal(err)
}
if len(past.Rows) != 0 {
t.Errorf("past-end page should be empty, got %v", past.Rows)
}
if past.Total != 4 {
t.Errorf("past-end total = %d, want 4", past.Total)
}
}
func TestRowsEmptyTable(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
page, err := db.Rows(ctx, "main", "items", 0, 10)
if err != nil {
t.Fatal(err)
}
if page.Total != 0 {
t.Errorf("empty table total = %d, want 0", page.Total)
}
if len(page.Rows) != 0 {
t.Errorf("empty table rows = %v, want none", page.Rows)
}
// Columns are still reported so a UI can render an empty grid.
wantCols := []string{"sku", "qty"}
if len(page.Columns) != 2 || page.Columns[0] != wantCols[0] || page.Columns[1] != wantCols[1] {
t.Errorf("empty table columns = %v, want %v", page.Columns, wantCols)
}
}
// TestRowsResolvesLongText guards the fix for out-of-line (address-encoded)
// text: a >4KB longtext column must render its actual content, not the
// "<binary>" placeholder that inline-only rendering produced.
func TestRowsResolvesLongText(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
page, err := db.Rows(ctx, "main", "docs", 0, 10)
if err != nil {
t.Fatal(err)
}
if len(page.Rows) != 1 {
t.Fatalf("docs rows = %d, want 1", len(page.Rows))
}
body := page.Rows[0][1] // columns: id, body
if body == placeholderBinary {
t.Fatalf("longtext rendered as %q; address-encoded text was not resolved", body)
}
if body != longDocBody {
t.Errorf("longtext body mismatch: got %d bytes, want %d bytes (prefix %.40q)",
len(body), len(longDocBody), body)
}
}
func TestCommitSummaryAddTable(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// C1 creates the empty users table.
diff, err := db.CommitSummary(ctx, commitHash(t, msgAddUser))
if err != nil {
t.Fatal(err)
}
td := findTable(t, diff, "users")
if !td.Added {
t.Errorf("users should be Added in C1: %+v", td)
}
if td.Dropped {
t.Errorf("users should not be Dropped: %+v", td)
}
if td.RowsAdded != 0 || td.RowsRemoved != 0 || td.RowsModified != 0 {
t.Errorf("C1 (empty table) row counts = %+v, want all 0", td)
}
}
func TestCommitSummaryInsertRows(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// C2 inserts 3 rows.
diff, err := db.CommitSummary(ctx, commitHash(t, msgInsert))
if err != nil {
t.Fatal(err)
}
td := findTable(t, diff, "users")
if td.Added || td.Dropped {
t.Errorf("C2 users should be a data change, not add/drop: %+v", td)
}
if td.RowsAdded != 3 {
t.Errorf("C2 RowsAdded = %d, want 3", td.RowsAdded)
}
if td.RowsRemoved != 0 || td.RowsModified != 0 {
t.Errorf("C2 unexpected removed/modified: %+v", td)
}
}
func TestCommitSummaryModifyRows(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// C3 updates id=1 and inserts id=4.
diff, err := db.CommitSummary(ctx, commitHash(t, msgModify))
if err != nil {
t.Fatal(err)
}
td := findTable(t, diff, "users")
if td.RowsAdded != 1 {
t.Errorf("C3 RowsAdded = %d, want 1", td.RowsAdded)
}
if td.RowsModified != 1 {
t.Errorf("C3 RowsModified = %d, want 1", td.RowsModified)
}
if td.RowsRemoved != 0 {
t.Errorf("C3 RowsRemoved = %d, want 0", td.RowsRemoved)
}
}
func TestCommitSummaryInitial(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
// C0 is the initial (parentless) commit, diffed against the empty root.
// WriteEmptyRepo's initial commit has no tables, so the summary is empty
// but must not error and must exercise the empty-root path.
diff, err := db.CommitSummary(ctx, commitHash(t, msgInitial))
if err != nil {
t.Fatalf("initial commit summary: %v", err)
}
if len(diff.Tables) != 0 {
t.Errorf("initial commit should have no table changes, got %+v", diff.Tables)
}
if diff.Hash != commitHash(t, msgInitial) {
t.Errorf("summary hash = %q, want %q", diff.Hash, commitHash(t, msgInitial))
}
}
func TestErrorsNonexistentRefAndTable(t *testing.T) {
db := openFixture(t)
ctx := context.Background()
if _, err := db.Tables(ctx, "no-such-ref"); !errors.Is(err, ErrRefNotFound) {
t.Errorf("Tables(bad ref) err = %v, want ErrRefNotFound", err)
}
if _, _, err := db.Log(ctx, "no-such-ref", "", 10); !errors.Is(err, ErrRefNotFound) {
t.Errorf("Log(bad ref) err = %v, want ErrRefNotFound", err)
}
if _, err := db.Rows(ctx, "main", "no-such-table", 0, 10); !errors.Is(err, ErrTableNotFound) {
t.Errorf("Rows(bad table) err = %v, want ErrTableNotFound", err)
}
}
func findTable(t *testing.T, diff *CommitDiff, name string) TableDiff {
t.Helper()
for _, td := range diff.Tables {
if td.Name == name {
return td
}
}
t.Fatalf("table %q not in diff %+v", name, diff.Tables)
return TableDiff{}
}
func equalRows(got, want [][]string) bool {
if len(got) != len(want) {
return false
}
for i := range got {
if len(got[i]) != len(want[i]) {
return false
}
for j := range got[i] {
if got[i][j] != want[i][j] {
return false
}
}
}
return true
}