summaryrefslogtreecommitdiff
path: root/internal/store
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-21 21:24:02 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-21 21:24:02 +0000
commit764d4d2d07449aec72c87afe941b7c63ea05e08c (patch)
tree042b157cfff2ab358b5bb9b891f26944133b7065 /internal/store
parent5f4f59fc6302a4e44773d4a939ae7b3304d61f1f (diff)
feat: Phase 1 — remove bug feature and dead code
- Delete Bug struct, BugToAtom, SourceBug, TypeBug, TypeNote - Remove bug store methods (SaveBug, GetBugs, ResolveBug, etc.) - Remove HandleGetBugs, HandleReportBug, bug branches in handlers - Remove bug routes, bugs.html template, bug UI from index.html - Remove AddMealToPlanner stub + interface method - Migration 018: DROP TABLE IF EXISTS bugs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/store')
-rw-r--r--internal/store/sqlite.go114
-rw-r--r--internal/store/sqlite_test.go188
2 files changed, 110 insertions, 192 deletions
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go
index 366b24e..33edbf2 100644
--- a/internal/store/sqlite.go
+++ b/internal/store/sqlite.go
@@ -73,20 +73,54 @@ func (s *Store) DB() *sql.DB {
return s.db
}
-// runMigrations executes all migration files in order
+// runMigrations executes all migration files in order, skipping already-applied ones.
func (s *Store) runMigrations() error {
- // Get migration files from configured directory
+ // Check if schema_migrations exists before creating it — used to detect legacy DBs.
+ var trackingExists int
+ s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'`).Scan(&trackingExists)
+
+ if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
+ filename TEXT PRIMARY KEY,
+ applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`); err != nil {
+ return fmt.Errorf("failed to create schema_migrations table: %w", err)
+ }
+
pattern := filepath.Join(s.migrationDir, "*.sql")
migrationFiles, err := filepath.Glob(pattern)
if err != nil {
return fmt.Errorf("failed to read migration files: %w", err)
}
- // Sort migrations by filename
sort.Strings(migrationFiles)
- // Execute each migration
+ // If schema_migrations was just created on a pre-existing DB (legacy), seed all
+ // current migration filenames as applied so we don't re-run them.
+ if trackingExists == 0 {
+ var existingTables int
+ s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name != 'schema_migrations'`).Scan(&existingTables)
+ if existingTables > 0 {
+ for _, file := range migrationFiles {
+ name := filepath.Base(file)
+ if _, err := s.db.Exec(`INSERT OR IGNORE INTO schema_migrations (filename) VALUES (?)`, name); err != nil {
+ return fmt.Errorf("failed to seed migration record %s: %w", name, err)
+ }
+ }
+ return nil
+ }
+ }
+
for _, file := range migrationFiles {
+ name := filepath.Base(file)
+
+ var count int
+ if err := s.db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE filename = ?`, name).Scan(&count); err != nil {
+ return fmt.Errorf("failed to check migration %s: %w", name, err)
+ }
+ if count > 0 {
+ continue // already applied
+ }
+
content, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("failed to read migration %s: %w", file, err)
@@ -95,6 +129,10 @@ func (s *Store) runMigrations() error {
if _, err := s.db.Exec(string(content)); err != nil {
return fmt.Errorf("failed to execute migration %s: %w", file, err)
}
+
+ if _, err := s.db.Exec(`INSERT INTO schema_migrations (filename) VALUES (?)`, name); err != nil {
+ return fmt.Errorf("failed to record migration %s: %w", name, err)
+ }
}
return nil
@@ -588,74 +626,6 @@ func (s *Store) ClearSyncToken(service string) error {
return err
}
-// Bug represents a user-reported bug
-type Bug struct {
- ID int64
- Description string
- CreatedAt time.Time
- ResolvedAt *time.Time
-}
-
-// SaveBug saves a new bug report
-func (s *Store) SaveBug(description string) error {
- _, err := s.db.Exec(`INSERT INTO bugs (description) VALUES (?)`, description)
- return err
-}
-
-// GetBugs retrieves all bugs, newest first
-func (s *Store) GetBugs() ([]Bug, error) {
- rows, err := s.db.Query(`SELECT id, description, created_at, resolved_at FROM bugs ORDER BY created_at DESC`)
- if err != nil {
- return nil, err
- }
- defer func() { _ = rows.Close() }()
-
- var bugs []Bug
- for rows.Next() {
- var b Bug
- var resolvedAt sql.NullTime
- if err := rows.Scan(&b.ID, &b.Description, &b.CreatedAt, &resolvedAt); err != nil {
- return nil, err
- }
- if resolvedAt.Valid {
- b.ResolvedAt = &resolvedAt.Time
- }
- bugs = append(bugs, b)
- }
- return bugs, rows.Err()
-}
-
-// GetUnresolvedBugs retrieves bugs that haven't been resolved yet
-func (s *Store) GetUnresolvedBugs() ([]Bug, error) {
- rows, err := s.db.Query(`SELECT id, description, created_at FROM bugs WHERE resolved_at IS NULL ORDER BY created_at DESC`)
- if err != nil {
- return nil, err
- }
- defer func() { _ = rows.Close() }()
-
- var bugs []Bug
- for rows.Next() {
- var b Bug
- if err := rows.Scan(&b.ID, &b.Description, &b.CreatedAt); err != nil {
- return nil, err
- }
- bugs = append(bugs, b)
- }
- return bugs, rows.Err()
-}
-
-// ResolveBug marks a bug as resolved
-func (s *Store) ResolveBug(id int64) error {
- _, err := s.db.Exec(`UPDATE bugs SET resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, id)
- return err
-}
-
-// UnresolveBug marks a bug as unresolved (reopens it)
-func (s *Store) UnresolveBug(id int64) error {
- _, err := s.db.Exec(`UPDATE bugs SET resolved_at = NULL WHERE id = ?`, id)
- return err
-}
-
// UserShoppingItem represents a user-added shopping item
type UserShoppingItem struct {
ID int64
diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go
index c6c428a..0514eeb 100644
--- a/internal/store/sqlite_test.go
+++ b/internal/store/sqlite_test.go
@@ -2,6 +2,7 @@ package store
import (
"database/sql"
+ "os"
"path/filepath"
"testing"
"time"
@@ -10,6 +11,73 @@ import (
"task-dashboard/internal/models"
)
+func TestRunMigrations_LegacyDB_SeedsTrackingTable(t *testing.T) {
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "test.db")
+ migDir := filepath.Join(tempDir, "migrations")
+ if err := os.MkdirAll(migDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ // Simulate a legacy DB: tables exist but no schema_migrations
+ db, err := sql.Open("sqlite3", dbPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.Exec("CREATE TABLE legacy_items (id INTEGER PRIMARY KEY); ALTER TABLE legacy_items ADD COLUMN created_at DATETIME;"); err != nil {
+ t.Fatal(err)
+ }
+ db.Close()
+
+ migration := []byte("CREATE TABLE legacy_items (id INTEGER PRIMARY KEY);\nALTER TABLE legacy_items ADD COLUMN created_at DATETIME;\n")
+ if err := os.WriteFile(filepath.Join(migDir, "001_test.sql"), migration, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // Should not fail even though migration SQL would fail if re-run
+ store, err := New(dbPath, migDir)
+ if err != nil {
+ t.Fatalf("New() on legacy DB failed: %v", err)
+ }
+ defer store.Close()
+
+ // Confirm migration was recorded as applied
+ var count int
+ store.db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE filename = '001_test.sql'").Scan(&count)
+ if count != 1 {
+ t.Errorf("expected migration to be seeded in schema_migrations, got count=%d", count)
+ }
+}
+
+func TestRunMigrations_IdempotentOnRestart(t *testing.T) {
+ tempDir := t.TempDir()
+ dbPath := filepath.Join(tempDir, "test.db")
+ migDir := filepath.Join(tempDir, "migrations")
+ if err := os.MkdirAll(migDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ // Write a migration with a non-idempotent ALTER TABLE
+ migration := []byte("CREATE TABLE foo (id INTEGER PRIMARY KEY);\nALTER TABLE foo ADD COLUMN bar TEXT;\n")
+ if err := os.WriteFile(filepath.Join(migDir, "001_test.sql"), migration, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // First run — should succeed
+ store1, err := New(dbPath, migDir)
+ if err != nil {
+ t.Fatalf("first New() failed: %v", err)
+ }
+ store1.Close()
+
+ // Second run (simulating service restart) — should also succeed
+ store2, err := New(dbPath, migDir)
+ if err != nil {
+ t.Fatalf("second New() failed (migration re-run not skipped): %v", err)
+ }
+ store2.Close()
+}
+
// setupTestStoreWithTasks creates a test store with tasks table
func setupTestStoreWithTasks(t *testing.T) *Store {
t.Helper()
@@ -631,126 +699,6 @@ func TestGetCardsByDateRange(t *testing.T) {
}
}
-// setupTestStoreWithBugs creates a test store with bugs table
-func setupTestStoreWithBugs(t *testing.T) *Store {
- t.Helper()
-
- tempDir := t.TempDir()
- dbPath := filepath.Join(tempDir, "test.db")
-
- db, err := sql.Open("sqlite3", dbPath)
- if err != nil {
- t.Fatalf("Failed to open test database: %v", err)
- }
-
- db.SetMaxOpenConns(1)
-
- store := &Store{db: db}
-
- schema := `
- CREATE TABLE IF NOT EXISTS bugs (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- description TEXT NOT NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- resolved_at DATETIME DEFAULT NULL
- );
- `
- if _, err := db.Exec(schema); err != nil {
- t.Fatalf("Failed to create schema: %v", err)
- }
-
- return store
-}
-
-func TestBugResolution(t *testing.T) {
- store := setupTestStoreWithBugs(t)
- defer func() { _ = store.Close() }()
-
- // Save some bugs
- if err := store.SaveBug("Bug 1"); err != nil {
- t.Fatalf("Failed to save bug 1: %v", err)
- }
- if err := store.SaveBug("Bug 2"); err != nil {
- t.Fatalf("Failed to save bug 2: %v", err)
- }
- if err := store.SaveBug("Bug 3"); err != nil {
- t.Fatalf("Failed to save bug 3: %v", err)
- }
-
- // Verify all 3 bugs are unresolved
- unresolved, err := store.GetUnresolvedBugs()
- if err != nil {
- t.Fatalf("GetUnresolvedBugs failed: %v", err)
- }
- if len(unresolved) != 3 {
- t.Errorf("Expected 3 unresolved bugs, got %d", len(unresolved))
- }
-
- // Resolve bug 2
- if err := store.ResolveBug(2); err != nil {
- t.Fatalf("ResolveBug failed: %v", err)
- }
-
- // Verify only 2 unresolved bugs remain
- unresolved, err = store.GetUnresolvedBugs()
- if err != nil {
- t.Fatalf("GetUnresolvedBugs after resolve failed: %v", err)
- }
- if len(unresolved) != 2 {
- t.Errorf("Expected 2 unresolved bugs, got %d", len(unresolved))
- }
-
- // Verify bug 2 is not in the unresolved list
- for _, bug := range unresolved {
- if bug.ID == 2 {
- t.Error("Bug 2 should have been resolved but is still in unresolved list")
- }
- }
-
- // Verify all bugs still exist in GetBugs (including resolved)
- allBugs, err := store.GetBugs()
- if err != nil {
- t.Fatalf("GetBugs failed: %v", err)
- }
- if len(allBugs) != 3 {
- t.Errorf("Expected 3 total bugs, got %d", len(allBugs))
- }
-
- // Verify bug 2 has resolved_at set
- for _, bug := range allBugs {
- if bug.ID == 2 {
- if bug.ResolvedAt == nil {
- t.Error("Bug 2 should have resolved_at set")
- }
- }
- }
-
- // Unresolve bug 2
- if err := store.UnresolveBug(2); err != nil {
- t.Fatalf("UnresolveBug failed: %v", err)
- }
-
- // Verify all 3 bugs are unresolved again
- unresolved, err = store.GetUnresolvedBugs()
- if err != nil {
- t.Fatalf("GetUnresolvedBugs after unresolve failed: %v", err)
- }
- if len(unresolved) != 3 {
- t.Errorf("Expected 3 unresolved bugs after unresolve, got %d", len(unresolved))
- }
-}
-
-func TestResolveBug_NonExistent(t *testing.T) {
- store := setupTestStoreWithBugs(t)
- defer func() { _ = store.Close() }()
-
- // Resolving a non-existent bug should not error (no rows affected is fine)
- err := store.ResolveBug(999)
- if err != nil {
- t.Errorf("ResolveBug on non-existent bug should not error, got: %v", err)
- }
-}
-
// =============================================================================
// User Shopping Items Tests
// =============================================================================