Fix store migration so the live database moves to the new scheme
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func openTestDB(t *testing.T, path string) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite", "file:"+path+"?_pragma=foreign_keys(1)&_txlock=immediate")
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func appliedNames(t *testing.T, db *sql.DB) []string {
|
||||
t.Helper()
|
||||
rows, err := db.Query(`SELECT name FROM migrations ORDER BY rowid`)
|
||||
if err != nil {
|
||||
t.Fatalf("list migrations: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
names := []string{}
|
||||
for rows.Next() {
|
||||
name := ""
|
||||
err = rows.Scan(&name)
|
||||
if err != nil {
|
||||
t.Fatalf("scan migration: %v", err)
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
t.Fatalf("list migrations: %v", rows.Err())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func migrationNames(ms []migration) []string {
|
||||
names := []string{}
|
||||
for _, m := range ms {
|
||||
names = append(names, m.name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func insertReport(t *testing.T, db *sql.DB, id, payload string) {
|
||||
t.Helper()
|
||||
_, err := db.Exec(`
|
||||
INSERT OR IGNORE INTO users (login, display_name, created_at) VALUES ('alice', 'Alice', 'x');
|
||||
INSERT OR IGNORE INTO projects (id, name, repo_path, default_branch, remote, created_by, created_at) VALUES ('p1', 'demo', '/nonexistent', 'main', 'none', 'alice', 'x');
|
||||
INSERT OR IGNORE INTO tasks (id, project_id, title, branch, worktree_path, base_ref, status, created_by, created_at) VALUES ('t1', 'p1', 't1', 't1', '/nonexistent', 'x', 'active', 'alice', 'x');
|
||||
INSERT OR IGNORE INTO agent_sessions (id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at) VALUES ('s1', 't1', 'claude', '', 's1', 'idle', 'implementer', 'alice', 'x');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert parents: %v", err)
|
||||
}
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO inbox_items (id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state, created_at)
|
||||
VALUES (?, 'p1', 't1', 's1', ?, 'report', 0, 0, ?, 'open', 'x')
|
||||
`, id, id, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("insert report: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func reportPayload(t *testing.T, db *sql.DB, id string) string {
|
||||
t.Helper()
|
||||
payload := ""
|
||||
err := db.QueryRow(`SELECT payload FROM inbox_items WHERE id = ?`, id).Scan(&payload)
|
||||
if err != nil {
|
||||
t.Fatalf("read report: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func TestMigrateFreshDatabase(t *testing.T) {
|
||||
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
||||
|
||||
err := migrate(db, migrations)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
err = migrate(db, migrations)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate again: %v", err)
|
||||
}
|
||||
|
||||
got := appliedNames(t, db)
|
||||
want := migrationNames(migrations)
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("applied %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateFromUserVersion(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "byre.db")
|
||||
db := openTestDB(t, path)
|
||||
for _, m := range migrations[:9] {
|
||||
_, err := db.Exec(m.sql)
|
||||
if err != nil {
|
||||
t.Fatalf("old migration %s: %v", m.name, err)
|
||||
}
|
||||
}
|
||||
_, err := db.Exec(`PRAGMA user_version = 9`)
|
||||
if err != nil {
|
||||
t.Fatalf("set user_version: %v", err)
|
||||
}
|
||||
insertReport(t, db, "r1", `{"text":"- shipped"}`)
|
||||
db.Close()
|
||||
|
||||
db = openTestDB(t, path)
|
||||
err = migrate(db, migrations)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
got := appliedNames(t, db)
|
||||
want := migrationNames(migrations)
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("applied %v, want %v", got, want)
|
||||
}
|
||||
payload := reportPayload(t, db, "r1")
|
||||
if payload != `{"text":"- shipped"}` {
|
||||
t.Fatalf("report payload %s, want it unchanged", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateRunsMigrationAddedBeforeApplied(t *testing.T) {
|
||||
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
||||
first := migration{"create-a", `CREATE TABLE a (x TEXT)`}
|
||||
other := migration{"create-b", `CREATE TABLE b (x TEXT)`}
|
||||
last := migration{"create-c", `CREATE TABLE c (x TEXT)`}
|
||||
|
||||
err := migrate(db, []migration{first, last})
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
err = migrate(db, []migration{first, other, last})
|
||||
if err != nil {
|
||||
t.Fatalf("migrate with other branch: %v", err)
|
||||
}
|
||||
|
||||
got := appliedNames(t, db)
|
||||
want := []string{"create-a", "create-c", "create-b"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("applied %v, want %v", got, want)
|
||||
}
|
||||
_, err = db.Exec(`INSERT INTO b (x) VALUES ('y')`)
|
||||
if err != nil {
|
||||
t.Fatalf("table from other branch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenReportPayloadsRunsTwice(t *testing.T) {
|
||||
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
||||
err := migrate(db, migrations)
|
||||
if err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
insertReport(t, db, "r1", `{"done":["one","two"]}`)
|
||||
|
||||
i := slices.IndexFunc(migrations, func(m migration) bool { return m.name == "flatten-report-payloads-to-text" })
|
||||
for range 2 {
|
||||
_, err = db.Exec(migrations[i].sql)
|
||||
if err != nil {
|
||||
t.Fatalf("flatten: %v", err)
|
||||
}
|
||||
payload := reportPayload(t, db, "r1")
|
||||
if payload != `{"text":"- one\n- two"}` {
|
||||
t.Fatalf("report payload %s", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user