refactor: dedupe error classification, fix --force+--source=db conflict
Post-merge cleanup of PR #805: 1. Extract duplicate error classification logic into classifyDatabaseError() helper function (was duplicated in two places in database.go) 2. Fix semantic conflict between --force and --source=db flags: - --force implies "database is broken, rebuild from JSONL" - --source=db implies "use database as source of truth" - These are contradictory; now errors with clear message - --force with --source=auto or --source=jsonl works as expected 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
@@ -249,65 +249,19 @@ func CheckDatabaseIntegrity(path string) DoctorCheck {
|
|||||||
// Open database in read-only mode for integrity check
|
// Open database in read-only mode for integrity check
|
||||||
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Classify the error type for better recovery guidance
|
|
||||||
errMsg := err.Error()
|
|
||||||
var errorType string
|
|
||||||
var recoverySteps string
|
|
||||||
|
|
||||||
// Check if JSONL recovery is possible
|
// Check if JSONL recovery is possible
|
||||||
jsonlCount, _, jsonlErr := CountJSONLIssues(filepath.Join(beadsDir, "issues.jsonl"))
|
jsonlCount, _, jsonlErr := CountJSONLIssues(filepath.Join(beadsDir, "issues.jsonl"))
|
||||||
if jsonlErr != nil {
|
if jsonlErr != nil {
|
||||||
jsonlCount, _, jsonlErr = CountJSONLIssues(filepath.Join(beadsDir, "beads.jsonl"))
|
jsonlCount, _, jsonlErr = CountJSONLIssues(filepath.Join(beadsDir, "beads.jsonl"))
|
||||||
}
|
}
|
||||||
|
jsonlAvailable := jsonlErr == nil && jsonlCount > 0
|
||||||
|
|
||||||
// Classify error type
|
errorType, recoverySteps := classifyDatabaseError(err.Error(), jsonlCount, jsonlAvailable)
|
||||||
if strings.Contains(errMsg, "database is locked") {
|
|
||||||
errorType = "Database is locked"
|
|
||||||
recoverySteps = "1. Check for running bd processes: ps aux | grep bd\n" +
|
|
||||||
"2. Kill any stale processes\n" +
|
|
||||||
"3. Remove stale locks: rm .beads/beads.db-shm .beads/beads.db-wal .beads/daemon.lock\n" +
|
|
||||||
"4. Retry: bd doctor --fix"
|
|
||||||
} else if strings.Contains(errMsg, "not a database") || strings.Contains(errMsg, "file is not a database") {
|
|
||||||
errorType = "File is not a valid SQLite database"
|
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Database file is corrupted beyond repair.\n\n"+
|
|
||||||
"Recovery steps:\n"+
|
|
||||||
"1. Backup corrupt database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
|
||||||
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
|
||||||
"3. Verify: bd stats", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Database file is corrupted and no JSONL backup found.\n" +
|
|
||||||
"Manual recovery required:\n" +
|
|
||||||
"1. Restore from git: git checkout HEAD -- .beads/issues.jsonl\n" +
|
|
||||||
"2. Rebuild database: bd doctor --fix --force"
|
|
||||||
}
|
|
||||||
} else if strings.Contains(errMsg, "migration") || strings.Contains(errMsg, "validation failed") {
|
|
||||||
errorType = "Database migration or validation failed"
|
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Database has validation errors (possibly orphaned dependencies).\n\n"+
|
|
||||||
"Recovery steps:\n"+
|
|
||||||
"1. Backup database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
|
||||||
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
|
||||||
"3. Verify: bd stats\n\n"+
|
|
||||||
"Alternative: bd doctor --fix --force (attempts to repair in-place)", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Database validation failed and no JSONL backup available.\n" +
|
|
||||||
"Try: bd doctor --fix --force"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorType = "Failed to open database"
|
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Run 'bd doctor --fix --source=jsonl' to rebuild from JSONL (%d issues)", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Run 'bd doctor --fix --force' to attempt recovery"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DoctorCheck{
|
return DoctorCheck{
|
||||||
Name: "Database Integrity",
|
Name: "Database Integrity",
|
||||||
Status: StatusError,
|
Status: StatusError,
|
||||||
Message: errorType,
|
Message: errorType,
|
||||||
Detail: fmt.Sprintf("%s\n\nError: %s", recoverySteps, errMsg),
|
Detail: fmt.Sprintf("%s\n\nError: %s", recoverySteps, err.Error()),
|
||||||
Fix: "See recovery steps above",
|
Fix: "See recovery steps above",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,65 +271,23 @@ func CheckDatabaseIntegrity(path string) DoctorCheck {
|
|||||||
// This checks the entire database for corruption
|
// This checks the entire database for corruption
|
||||||
rows, err := db.Query("PRAGMA integrity_check")
|
rows, err := db.Query("PRAGMA integrity_check")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Classify the error type for better recovery guidance
|
|
||||||
errMsg := err.Error()
|
|
||||||
var errorType string
|
|
||||||
var recoverySteps string
|
|
||||||
|
|
||||||
// Check if JSONL recovery is possible
|
// Check if JSONL recovery is possible
|
||||||
jsonlCount, _, jsonlErr := CountJSONLIssues(filepath.Join(beadsDir, "issues.jsonl"))
|
jsonlCount, _, jsonlErr := CountJSONLIssues(filepath.Join(beadsDir, "issues.jsonl"))
|
||||||
if jsonlErr != nil {
|
if jsonlErr != nil {
|
||||||
jsonlCount, _, jsonlErr = CountJSONLIssues(filepath.Join(beadsDir, "beads.jsonl"))
|
jsonlCount, _, jsonlErr = CountJSONLIssues(filepath.Join(beadsDir, "beads.jsonl"))
|
||||||
}
|
}
|
||||||
|
jsonlAvailable := jsonlErr == nil && jsonlCount > 0
|
||||||
|
|
||||||
// Classify error type (same logic as opening errors)
|
errorType, recoverySteps := classifyDatabaseError(err.Error(), jsonlCount, jsonlAvailable)
|
||||||
if strings.Contains(errMsg, "database is locked") {
|
// Override default error type for this specific case
|
||||||
errorType = "Database is locked"
|
if errorType == "Failed to open database" {
|
||||||
recoverySteps = "1. Check for running bd processes: ps aux | grep bd\n" +
|
|
||||||
"2. Kill any stale processes\n" +
|
|
||||||
"3. Remove stale locks: rm .beads/beads.db-shm .beads/beads.db-wal .beads/daemon.lock\n" +
|
|
||||||
"4. Retry: bd doctor --fix"
|
|
||||||
} else if strings.Contains(errMsg, "not a database") || strings.Contains(errMsg, "file is not a database") {
|
|
||||||
errorType = "File is not a valid SQLite database"
|
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Database file is corrupted beyond repair.\n\n"+
|
|
||||||
"Recovery steps:\n"+
|
|
||||||
"1. Backup corrupt database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
|
||||||
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
|
||||||
"3. Verify: bd stats", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Database file is corrupted and no JSONL backup found.\n" +
|
|
||||||
"Manual recovery required:\n" +
|
|
||||||
"1. Restore from git: git checkout HEAD -- .beads/issues.jsonl\n" +
|
|
||||||
"2. Rebuild database: bd doctor --fix --force"
|
|
||||||
}
|
|
||||||
} else if strings.Contains(errMsg, "migration") || strings.Contains(errMsg, "validation failed") {
|
|
||||||
errorType = "Database migration or validation failed"
|
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Database has validation errors (possibly orphaned dependencies).\n\n"+
|
|
||||||
"Recovery steps:\n"+
|
|
||||||
"1. Backup database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
|
||||||
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
|
||||||
"3. Verify: bd stats\n\n"+
|
|
||||||
"Alternative: bd doctor --fix --force (attempts to repair in-place)", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Database validation failed and no JSONL backup available.\n" +
|
|
||||||
"Try: bd doctor --fix --force"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorType = "Failed to run integrity check"
|
errorType = "Failed to run integrity check"
|
||||||
if jsonlErr == nil && jsonlCount > 0 {
|
|
||||||
recoverySteps = fmt.Sprintf("Run 'bd doctor --fix --force --source=jsonl' to rebuild from JSONL (%d issues)", jsonlCount)
|
|
||||||
} else {
|
|
||||||
recoverySteps = "Run 'bd doctor --fix --force' to attempt recovery"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DoctorCheck{
|
return DoctorCheck{
|
||||||
Name: "Database Integrity",
|
Name: "Database Integrity",
|
||||||
Status: StatusError,
|
Status: StatusError,
|
||||||
Message: errorType,
|
Message: errorType,
|
||||||
Detail: fmt.Sprintf("%s\n\nError: %s", recoverySteps, errMsg),
|
Detail: fmt.Sprintf("%s\n\nError: %s", recoverySteps, err.Error()),
|
||||||
Fix: "See recovery steps above",
|
Fix: "See recovery steps above",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,6 +561,57 @@ func FixDBJSONLSync(path string) error {
|
|||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
|
// classifyDatabaseError classifies a database error and returns appropriate recovery guidance.
|
||||||
|
// Returns the error type description and recovery steps based on error message and JSONL availability.
|
||||||
|
func classifyDatabaseError(errMsg string, jsonlCount int, jsonlAvailable bool) (errorType, recoverySteps string) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errMsg, "database is locked"):
|
||||||
|
errorType = "Database is locked"
|
||||||
|
recoverySteps = "1. Check for running bd processes: ps aux | grep bd\n" +
|
||||||
|
"2. Kill any stale processes\n" +
|
||||||
|
"3. Remove stale locks: rm .beads/beads.db-shm .beads/beads.db-wal .beads/daemon.lock\n" +
|
||||||
|
"4. Retry: bd doctor --fix"
|
||||||
|
|
||||||
|
case strings.Contains(errMsg, "not a database") || strings.Contains(errMsg, "file is not a database"):
|
||||||
|
errorType = "File is not a valid SQLite database"
|
||||||
|
if jsonlAvailable {
|
||||||
|
recoverySteps = fmt.Sprintf("Database file is corrupted beyond repair.\n\n"+
|
||||||
|
"Recovery steps:\n"+
|
||||||
|
"1. Backup corrupt database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
||||||
|
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
||||||
|
"3. Verify: bd stats", jsonlCount)
|
||||||
|
} else {
|
||||||
|
recoverySteps = "Database file is corrupted and no JSONL backup found.\n" +
|
||||||
|
"Manual recovery required:\n" +
|
||||||
|
"1. Restore from git: git checkout HEAD -- .beads/issues.jsonl\n" +
|
||||||
|
"2. Rebuild database: bd doctor --fix --force"
|
||||||
|
}
|
||||||
|
|
||||||
|
case strings.Contains(errMsg, "migration") || strings.Contains(errMsg, "validation failed"):
|
||||||
|
errorType = "Database migration or validation failed"
|
||||||
|
if jsonlAvailable {
|
||||||
|
recoverySteps = fmt.Sprintf("Database has validation errors (possibly orphaned dependencies).\n\n"+
|
||||||
|
"Recovery steps:\n"+
|
||||||
|
"1. Backup database: mv .beads/beads.db .beads/beads.db.broken\n"+
|
||||||
|
"2. Rebuild from JSONL (%d issues): bd doctor --fix --force --source=jsonl\n"+
|
||||||
|
"3. Verify: bd stats\n\n"+
|
||||||
|
"Alternative: bd doctor --fix --force (attempts to repair in-place)", jsonlCount)
|
||||||
|
} else {
|
||||||
|
recoverySteps = "Database validation failed and no JSONL backup available.\n" +
|
||||||
|
"Try: bd doctor --fix --force"
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorType = "Failed to open database"
|
||||||
|
if jsonlAvailable {
|
||||||
|
recoverySteps = fmt.Sprintf("Run 'bd doctor --fix --source=jsonl' to rebuild from JSONL (%d issues)", jsonlCount)
|
||||||
|
} else {
|
||||||
|
recoverySteps = "Run 'bd doctor --fix --force' to attempt recovery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// getDatabaseVersionFromPath reads the database version from the given path
|
// getDatabaseVersionFromPath reads the database version from the given path
|
||||||
func getDatabaseVersionFromPath(dbPath string) string {
|
func getDatabaseVersionFromPath(dbPath string) string {
|
||||||
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ func DatabaseCorruptionRecoveryWithOptions(path string, force bool, source strin
|
|||||||
jsonlPath := findJSONLPath(beadsDir)
|
jsonlPath := findJSONLPath(beadsDir)
|
||||||
jsonlExists := jsonlPath != ""
|
jsonlExists := jsonlPath != ""
|
||||||
|
|
||||||
|
// Check for contradictory flags early
|
||||||
|
if force && source == "db" {
|
||||||
|
return fmt.Errorf("--force and --source=db are contradictory: --force implies the database is broken and recovery should use JSONL. Use --source=jsonl or --source=auto with --force")
|
||||||
|
}
|
||||||
|
|
||||||
// Determine source of truth based on --source flag and availability
|
// Determine source of truth based on --source flag and availability
|
||||||
var useJSONL bool
|
var useJSONL bool
|
||||||
switch source {
|
switch source {
|
||||||
@@ -117,17 +122,28 @@ func DatabaseCorruptionRecoveryWithOptions(path string, force bool, source strin
|
|||||||
return fmt.Errorf("--source=jsonl specified but no JSONL file found")
|
return fmt.Errorf("--source=jsonl specified but no JSONL file found")
|
||||||
}
|
}
|
||||||
useJSONL = true
|
useJSONL = true
|
||||||
fmt.Println(" Using JSONL as source of truth (--source=jsonl)")
|
if force {
|
||||||
|
fmt.Println(" Using JSONL as source of truth (--force --source=jsonl)")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" Using JSONL as source of truth (--source=jsonl)")
|
||||||
|
}
|
||||||
case "db":
|
case "db":
|
||||||
// Explicit database preference
|
// Explicit database preference (already checked for force+db contradiction above)
|
||||||
if !dbExists {
|
if !dbExists {
|
||||||
return fmt.Errorf("--source=db specified but no database found")
|
return fmt.Errorf("--source=db specified but no database found")
|
||||||
}
|
}
|
||||||
useJSONL = false
|
useJSONL = false
|
||||||
fmt.Println(" Using database as source of truth (--source=db)")
|
fmt.Println(" Using database as source of truth (--source=db)")
|
||||||
case "auto":
|
case "auto":
|
||||||
// Auto-detect: prefer JSONL if database is corrupted, otherwise prefer database
|
// Auto-detect: prefer JSONL if database is corrupted or force is set
|
||||||
if !dbExists && jsonlExists {
|
if force {
|
||||||
|
// Force mode implies database is broken - use JSONL
|
||||||
|
if !jsonlExists {
|
||||||
|
return fmt.Errorf("--force requires JSONL for recovery but no JSONL file found")
|
||||||
|
}
|
||||||
|
useJSONL = true
|
||||||
|
fmt.Println(" Using JSONL as source of truth (--force mode)")
|
||||||
|
} else if !dbExists && jsonlExists {
|
||||||
useJSONL = true
|
useJSONL = true
|
||||||
fmt.Println(" Using JSONL as source of truth (database missing)")
|
fmt.Println(" Using JSONL as source of truth (database missing)")
|
||||||
} else if dbExists && !jsonlExists {
|
} else if dbExists && !jsonlExists {
|
||||||
@@ -144,12 +160,6 @@ func DatabaseCorruptionRecoveryWithOptions(path string, force bool, source strin
|
|||||||
return fmt.Errorf("invalid --source value: %s (valid values: auto, jsonl, db)", source)
|
return fmt.Errorf("invalid --source value: %s (valid values: auto, jsonl, db)", source)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If force mode is enabled, always use JSONL recovery (bypass database validation)
|
|
||||||
if force {
|
|
||||||
useJSONL = true
|
|
||||||
fmt.Println(" Force mode enabled: bypassing database validation, using JSONL recovery")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If using database as source, just run migration (no recovery needed)
|
// If using database as source, just run migration (no recovery needed)
|
||||||
if !useJSONL {
|
if !useJSONL {
|
||||||
fmt.Println(" Database is the source of truth - skipping recovery")
|
fmt.Println(" Database is the source of truth - skipping recovery")
|
||||||
|
|||||||
Reference in New Issue
Block a user