diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-21 21:24:02 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-21 21:24:02 +0000 |
| commit | 764d4d2d07449aec72c87afe941b7c63ea05e08c (patch) | |
| tree | 042b157cfff2ab358b5bb9b891f26944133b7065 /internal/store | |
| parent | 5f4f59fc6302a4e44773d4a939ae7b3304d61f1f (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.go | 114 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 188 |
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 // ============================================================================= |
