Files
beads/internal/storage/sqlite/child_counters_migration_test.go

164 lines
4.0 KiB
Go

package sqlite
import (
"context"
"database/sql"
"os"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestMigrateChildCountersTable(t *testing.T) {
t.Run("creates child_counters table if not exists", func(t *testing.T) {
// Create a temp database without child_counters table
dbFile, err := os.CreateTemp("", "beads_test_*.db")
if err != nil {
t.Fatalf("failed to create temp db: %v", err)
}
defer os.Remove(dbFile.Name())
dbFile.Close()
db, err := sql.Open("sqlite3", dbFile.Name())
if err != nil {
t.Fatalf("failed to open db: %v", err)
}
defer db.Close()
// Create minimal schema without child_counters
_, err = db.Exec(`
CREATE TABLE issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open'
);
`)
if err != nil {
t.Fatalf("failed to create issues table: %v", err)
}
// Verify child_counters doesn't exist
var count int
err = db.QueryRow(`
SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='child_counters'
`).Scan(&count)
if err != nil {
t.Fatalf("failed to check for child_counters table: %v", err)
}
if count != 0 {
t.Fatalf("child_counters table should not exist yet, got count %d", count)
}
// Run migration
err = migrateChildCountersTable(db)
if err != nil {
t.Fatalf("migration failed: %v", err)
}
// Verify child_counters exists
err = db.QueryRow(`
SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='child_counters'
`).Scan(&count)
if err != nil {
t.Fatalf("failed to check for child_counters table: %v", err)
}
if count == 0 {
t.Fatalf("child_counters table not created, got count %d", count)
}
})
t.Run("is idempotent", func(t *testing.T) {
// Create storage with full schema (including child_counters)
s := newTestStore(t, "")
defer s.Close()
db := s.db
// Run migration twice
err := migrateChildCountersTable(db)
if err != nil {
t.Fatalf("first migration failed: %v", err)
}
err = migrateChildCountersTable(db)
if err != nil {
t.Fatalf("second migration failed (not idempotent): %v", err)
}
// Verify table exists
var count int
err = db.QueryRow(`
SELECT COUNT(*) FROM sqlite_master
WHERE type='table' AND name='child_counters'
`).Scan(&count)
if err != nil {
t.Fatalf("failed to check for child_counters table: %v", err)
}
if count == 0 {
t.Fatalf("child_counters table not found, got count %d", count)
}
})
t.Run("has ON DELETE CASCADE constraint", func(t *testing.T) {
// Create storage with full schema
s := newTestStore(t, "")
defer s.Close()
ctx := context.Background()
// Create a parent issue
parent := &types.Issue{
ID: "bd-parent",
Title: "Parent",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeEpic,
}
err := s.CreateIssue(ctx, parent, "test")
if err != nil {
t.Fatalf("failed to create parent issue: %v", err)
}
// Generate child ID (this populates child_counters)
childID, err := s.GetNextChildID(ctx, "bd-parent")
if err != nil {
t.Fatalf("failed to get next child ID: %v", err)
}
if childID != "bd-parent.1" {
t.Fatalf("expected bd-parent.1, got %s", childID)
}
// Verify child_counters has entry for parent
var lastChild int
err = s.db.QueryRow(`
SELECT last_child FROM child_counters WHERE parent_id = ?
`, "bd-parent").Scan(&lastChild)
if err != nil {
t.Fatalf("failed to query child_counters: %v", err)
}
if lastChild != 1 {
t.Fatalf("expected last_child=1, got %d", lastChild)
}
// Delete the parent issue
err = s.DeleteIssue(ctx, "bd-parent")
if err != nil {
t.Fatalf("failed to delete parent issue: %v", err)
}
// Verify child_counters entry was CASCADE deleted
var count int
err = s.db.QueryRow(`
SELECT COUNT(*) FROM child_counters WHERE parent_id = ?
`, "bd-parent").Scan(&count)
if err != nil {
t.Fatalf("failed to query child_counters: %v", err)
}
if count != 0 {
t.Fatalf("expected child_counters entry to be deleted, found %d entries", count)
}
})
}