bd-162: Add database integrity checks with oracle review fixes
- Added validatePreExport to prevent data loss - Added checkDuplicateIDs to detect corruption - Added checkOrphanedDeps to find orphaned dependencies (both sides) - Added validatePostImport to ensure imports don't lose data - CRITICAL FIX: Removed post-pull export that clobbered fresh JSONL - Conservative checks when JSONL is unreadable - Efficient COUNT(*) SQL path instead of loading all issues - Comprehensive test coverage including edge cases
This commit is contained in:
@@ -988,6 +988,25 @@ func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, auto
|
||||
log.log("Removed stale lock (%s), proceeding with sync", holder)
|
||||
}
|
||||
|
||||
// Integrity check: validate before export
|
||||
if err := validatePreExport(syncCtx, store, jsonlPath); err != nil {
|
||||
log.log("Pre-export validation failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate IDs (database corruption)
|
||||
if err := checkDuplicateIDs(syncCtx, store); err != nil {
|
||||
log.log("Duplicate ID check failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for orphaned dependencies (warns but doesn't fail)
|
||||
if orphaned, err := checkOrphanedDeps(syncCtx, store); err != nil {
|
||||
log.log("Orphaned dependency check failed: %v", err)
|
||||
} else if len(orphaned) > 0 {
|
||||
log.log("Found %d orphaned dependencies: %v", len(orphaned), orphaned)
|
||||
}
|
||||
|
||||
if err := exportToJSONLWithStore(syncCtx, store, jsonlPath); err != nil {
|
||||
log.log("Export failed: %v", err)
|
||||
return
|
||||
@@ -1017,13 +1036,12 @@ func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, auto
|
||||
}
|
||||
log.log("Pulled from remote")
|
||||
|
||||
// Flush any pending dirty issues to JSONL BEFORE importing
|
||||
// This prevents the race condition where deletions get re-imported (bd-155)
|
||||
if err := exportToJSONLWithStore(syncCtx, store, jsonlPath); err != nil {
|
||||
log.log("Pre-import flush failed: %v", err)
|
||||
return
|
||||
// Count issues before import for validation
|
||||
beforeCount, err := countDBIssues(syncCtx, store)
|
||||
if err != nil {
|
||||
log.log("Failed to count issues before import: %v", err)
|
||||
return
|
||||
}
|
||||
log.log("Flushed pending changes before import")
|
||||
|
||||
if err := importToJSONLWithStore(syncCtx, store, jsonlPath); err != nil {
|
||||
log.log("Import failed: %v", err)
|
||||
@@ -1031,6 +1049,18 @@ func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, auto
|
||||
}
|
||||
log.log("Imported from JSONL")
|
||||
|
||||
// Validate import didn't cause data loss
|
||||
afterCount, err := countDBIssues(syncCtx, store)
|
||||
if err != nil {
|
||||
log.log("Failed to count issues after import: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validatePostImport(beforeCount, afterCount); err != nil {
|
||||
log.log("Post-import validation failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if autoPush && autoCommit {
|
||||
if err := gitPush(syncCtx); err != nil {
|
||||
log.log("Push failed: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user