From 8d4f664b91f6fe6e138c042c5523b8e9ac7972a2 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 3 Dec 2025 00:11:19 -0800 Subject: [PATCH] feat(doctor): add SQLite integrity check (bd-2au) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run PRAGMA integrity_check to detect database corruption. Reports any issues found and suggests recovery options. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cmd/bd/doctor.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index 856b90fe..6db12f08 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -536,6 +536,13 @@ func runDiagnostics(path string) doctorResult { result.OverallOK = false } + // Check 2b: Database integrity (bd-2au) + integrityCheck := checkDatabaseIntegrity(path) + result.Checks = append(result.Checks, integrityCheck) + if integrityCheck.Status == statusError { + result.OverallOK = false + } + // Check 3: ID format (hash vs sequential) idCheck := checkIDFormat(path) result.Checks = append(result.Checks, idCheck) @@ -1885,6 +1892,80 @@ func checkSchemaCompatibility(path string) doctorCheck { } } +// checkDatabaseIntegrity runs SQLite's PRAGMA integrity_check (bd-2au) +func checkDatabaseIntegrity(path string) doctorCheck { + beadsDir := filepath.Join(path, ".beads") + + // Get database path (same logic as checkSchemaCompatibility) + var dbPath string + if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" { + dbPath = cfg.DatabasePath(beadsDir) + } else { + dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName) + } + + // If no database, skip this check + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + return doctorCheck{ + Name: "Database Integrity", + Status: statusOK, + Message: "N/A (no database)", + } + } + + // Open database in read-only mode for integrity check + db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro&_pragma=busy_timeout(30000)") + if err != nil { + return doctorCheck{ + Name: "Database Integrity", + Status: statusError, + Message: "Failed to open database for integrity check", + Detail: err.Error(), + } + } + defer db.Close() + + // Run PRAGMA integrity_check + // This checks the entire database for corruption + rows, err := db.Query("PRAGMA integrity_check") + if err != nil { + return doctorCheck{ + Name: "Database Integrity", + Status: statusError, + Message: "Failed to run integrity check", + Detail: err.Error(), + } + } + defer rows.Close() + + var results []string + for rows.Next() { + var result string + if err := rows.Scan(&result); err != nil { + continue + } + results = append(results, result) + } + + // "ok" means no corruption detected + if len(results) == 1 && results[0] == "ok" { + return doctorCheck{ + Name: "Database Integrity", + Status: statusOK, + Message: "No corruption detected", + } + } + + // Any other result indicates corruption + return doctorCheck{ + Name: "Database Integrity", + Status: statusError, + Message: "Database corruption detected", + Detail: strings.Join(results, "; "), + Fix: "Database may need recovery. Export with 'bd export' if possible, then restore from backup or reinitialize", + } +} + func checkMergeDriver(path string) doctorCheck { // Check if we're in a git repository gitDir := filepath.Join(path, ".git")