Skip to content
go-libfossil Artifact [6416c0f289]
Login

Artifact [6416c0f289]

Artifact 6416c0f289c1a7f6aaad82b61d4d5da52ac1c62f16d935c66b85384d54fe4851:


package db_test

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/danmestas/libfossil/db"
	_ "github.com/danmestas/libfossil/internal/testdriver"
	"github.com/danmestas/libfossil/simio"
)

func TestOpenClose(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	var result int
	err = d.QueryRow("SELECT 1+1").Scan(&result)
	if err != nil {
		t.Fatalf("QueryRow: %v", err)
	}
	if result != 2 {
		t.Fatalf("got %d, want 2", result)
	}
}
func TestOpenSQLMergesURIParams(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	conn, err := db.OpenSQL(path, db.OpenConfig{}, nil)
	if err != nil {
		t.Fatalf("OpenSQL create: %v", err)
	}
	if _, err := conn.Exec("CREATE TABLE t(v INTEGER)"); err != nil {
		conn.Close()
		t.Fatalf("create table: %v", err)
	}
	if err := conn.Close(); err != nil {
		t.Fatalf("close create conn: %v", err)
	}

	driver, dsn := db.SQLDriverAndDSN(path, db.OpenConfig{}, map[string]string{"mode": "ro"})
	if driver == "" {
		t.Fatal("empty driver")
	}
	if strings.Count(dsn, "?") != 1 {
		t.Fatalf("DSN %q contains %d query delimiters, want 1", dsn, strings.Count(dsn, "?"))
	}
	if !strings.Contains(dsn, "mode=ro") {
		t.Fatalf("DSN %q missing mode=ro", dsn)
	}

	ro, err := db.OpenSQL(path, db.OpenConfig{}, map[string]string{"mode": "ro"})
	if err != nil {
		t.Fatalf("OpenSQL read-only: %v", err)
	}
	defer ro.Close()
	if _, err := ro.Exec("INSERT INTO t(v) VALUES(1)"); err == nil {
		t.Fatal("read-only OpenSQL accepted a write")
	}
}

func TestExec(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	_, err = d.Exec("CREATE TABLE test(id INTEGER PRIMARY KEY, val TEXT)")
	if err != nil {
		t.Fatalf("Exec CREATE: %v", err)
	}

	_, err = d.Exec("INSERT INTO test(val) VALUES(?)", "hello")
	if err != nil {
		t.Fatalf("Exec INSERT: %v", err)
	}

	var val string
	err = d.QueryRow("SELECT val FROM test WHERE id=1").Scan(&val)
	if err != nil {
		t.Fatalf("QueryRow: %v", err)
	}
	if val != "hello" {
		t.Fatalf("val = %q, want %q", val, "hello")
	}
}

func TestApplicationID(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	err = d.SetApplicationID(252006673)
	if err != nil {
		t.Fatalf("SetApplicationID: %v", err)
	}

	id, err := d.ApplicationID()
	if err != nil {
		t.Fatalf("ApplicationID: %v", err)
	}
	if id != 252006673 {
		t.Fatalf("application_id = %d, want 252006673", id)
	}
}

func TestTransaction(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	if _, err := d.Exec("CREATE TABLE test(id INTEGER PRIMARY KEY, val TEXT)"); err != nil {
		t.Fatalf("Exec CREATE: %v", err)
	}

	// Commit case
	err = d.WithTx(func(tx *db.Tx) error {
		_, err := tx.Exec("INSERT INTO test(val) VALUES(?)", "committed")
		return err
	})
	if err != nil {
		t.Fatalf("WithTx commit: %v", err)
	}

	var count int
	if err := d.QueryRow("SELECT count(*) FROM test").Scan(&count); err != nil {
		t.Fatalf("count query: %v", err)
	}
	if count != 1 {
		t.Fatalf("count after commit = %d, want 1", count)
	}

	// Rollback case
	err = d.WithTx(func(tx *db.Tx) error {
		if _, err := tx.Exec("INSERT INTO test(val) VALUES(?)", "rolled-back"); err != nil {
			return err
		}
		return fmt.Errorf("deliberate error")
	})
	if err == nil {
		t.Fatal("WithTx should return error")
	}

	if err := d.QueryRow("SELECT count(*) FROM test").Scan(&count); err != nil {
		t.Fatalf("count query after rollback: %v", err)
	}
	if count != 1 {
		t.Fatalf("count after rollback = %d, want 1", count)
	}
}

func TestCreateRepoSchema(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.fossil")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	err = db.CreateRepoSchema(d)
	if err != nil {
		t.Fatalf("CreateRepoSchema: %v", err)
	}

	// Verify repo1 (static) tables exist
	repo1Tables := []string{"blob", "delta", "rcvfrom", "user", "config", "shun", "private", "reportfmt", "concealed"}
	for _, table := range repo1Tables {
		var name string
		err := d.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
		if err != nil {
			t.Errorf("repo1 table %q not found: %v", table, err)
		}
	}

	// Verify repo2 (transient) tables exist
	repo2Tables := []string{"filename", "mlink", "plink", "leaf", "event", "phantom", "orphan", "unclustered", "unsent", "tag", "tagxref", "backlink", "attachment", "cherrypick", "forumpost"}
	for _, table := range repo2Tables {
		var name string
		err := d.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
		if err != nil {
			t.Errorf("repo2 table %q not found: %v", table, err)
		}
	}

	// Verify application_id
	id, err := d.ApplicationID()
	if err != nil {
		t.Fatalf("ApplicationID: %v", err)
	}
	if id != 252006673 {
		t.Fatalf("application_id = %d, want 252006673", id)
	}

	// Verify seed rcvfrom row
	var rcvid int
	err = d.QueryRow("SELECT rcvid FROM rcvfrom WHERE rcvid=1").Scan(&rcvid)
	if err != nil {
		t.Fatalf("seed rcvfrom row missing: %v", err)
	}

	// Verify seed tag rows (1-11)
	var tagCount int
	err = d.QueryRow("SELECT count(*) FROM tag").Scan(&tagCount)
	if err != nil {
		t.Fatalf("tag count: %v", err)
	}
	if tagCount != 11 {
		t.Fatalf("tag count = %d, want 11", tagCount)
	}

	// Verify specific seed tags
	var tagName string
	err = d.QueryRow("SELECT tagname FROM tag WHERE tagid=8").Scan(&tagName)
	if err != nil {
		t.Fatalf("tag 8: %v", err)
	}
	if tagName != "branch" {
		t.Fatalf("tag 8 name = %q, want %q", tagName, "branch")
	}
}

func TestCreateRepoSchema_FossilValidation(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping fossil CLI validation in short mode")
	}

	path := filepath.Join(t.TempDir(), "test.fossil")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}

	err = db.CreateRepoSchema(d)
	if err != nil {
		t.Fatalf("CreateRepoSchema: %v", err)
	}

	err = db.SeedConfig(d, simio.CryptoRand{}, "")
	if err != nil {
		t.Fatalf("SeedConfig: %v", err)
	}

	err = db.SeedUser(d, "testuser")
	if err != nil {
		t.Fatalf("SeedUser: %v", err)
	}

	d.Close()

	// fossil rebuild should pass on Go-created schema
	cmd := exec.Command("fossil", "rebuild", path)
	out, err := cmd.CombinedOutput()
	if err != nil {
		t.Fatalf("fossil rebuild failed: %v\n%s", err, out)
	}
}

func TestRegisterDriver(t *testing.T) {
	// Driver should already be registered by testdriver import (added in Task 3).
	cfg := db.RegisteredDriver()
	if cfg == nil {
		t.Fatal("no driver registered")
	}
	if cfg.Name == "" {
		t.Fatal("registered driver name is empty")
	}
	t.Logf("active driver: %s", cfg.Name)

	dsn := cfg.BuildDSN("/tmp/test.db", db.DefaultPragmas())
	t.Logf("DSN: %s", dsn)
	if !strings.Contains(dsn, "journal_mode") {
		t.Fatal("DSN missing journal_mode pragma")
	}
	if !strings.Contains(dsn, "busy_timeout") {
		t.Fatal("DSN missing busy_timeout pragma")
	}
}

func TestOpenWithDefaults(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.Open(path)
	if err != nil {
		t.Fatalf("Open: %v", err)
	}
	defer d.Close()

	if d.Driver() == "" {
		t.Fatal("Driver() returned empty string")
	}
	t.Logf("opened with driver: %s", d.Driver())

	var mode string
	if err := d.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil {
		t.Fatalf("journal_mode query: %v", err)
	}
	if mode != "wal" {
		t.Fatalf("journal_mode = %q, want wal", mode)
	}
}

func TestOpenWithCustomPragmas(t *testing.T) {
	path := filepath.Join(t.TempDir(), "test.db")
	d, err := db.OpenWith(path, db.OpenConfig{
		Pragmas: map[string]string{
			"cache_size": "-2000",
		},
	})
	if err != nil {
		t.Fatalf("OpenWith: %v", err)
	}
	defer d.Close()

	var mode string
	if err := d.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil {
		t.Fatalf("journal_mode query: %v", err)
	}
	if mode != "wal" {
		t.Fatalf("journal_mode = %q, want wal (default should still apply)", mode)
	}
}