Cleanup and fixes: godoc comments, removed dead code, fixed renumber FK constraint bug
- Added comprehensive godoc comments for auto-flush functions (bd-4) - Removed unused issueMap in scoreCollisions (bd-6) - Fixed renumber command FK constraint failure (bd-143) - Changed UpdateIssueID to use explicit connection with FK disabled - Resolves 'constraint failed: FOREIGN KEY constraint failed' error - Deleted 22 test/placeholder issues - Renumbered issues from bd-1 to bd-143 (eliminated gaps) Amp-Thread-ID: https://ampcode.com/threads/T-65f78f08-4856-4af0-9d6c-af33e88b5f63 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -745,6 +745,15 @@ func outputJSON(v interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findJSONLPath finds the JSONL file path for the current database
|
// findJSONLPath finds the JSONL file path for the current database
|
||||||
|
// findJSONLPath discovers the JSONL file path for the current database and ensures
|
||||||
|
// the parent directory exists. Uses beads.FindJSONLPath() for discovery (checking
|
||||||
|
// BEADS_JSONL env var first, then using .beads/issues.jsonl next to the database).
|
||||||
|
//
|
||||||
|
// Creates the .beads directory if it doesn't exist (important for new databases).
|
||||||
|
// If directory creation fails, returns the path anyway - the subsequent write will
|
||||||
|
// fail with a clearer error message.
|
||||||
|
//
|
||||||
|
// Thread-safe: No shared state access.
|
||||||
func findJSONLPath() string {
|
func findJSONLPath() string {
|
||||||
// Use public API for path discovery
|
// Use public API for path discovery
|
||||||
jsonlPath := beads.FindJSONLPath(dbPath)
|
jsonlPath := beads.FindJSONLPath(dbPath)
|
||||||
@@ -985,6 +994,19 @@ func checkVersionMismatch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// markDirtyAndScheduleFlush marks the database as dirty and schedules a flush
|
// markDirtyAndScheduleFlush marks the database as dirty and schedules a flush
|
||||||
|
// markDirtyAndScheduleFlush marks the database as dirty and schedules a debounced
|
||||||
|
// export to JSONL. Uses a timer that resets on each call - flush occurs 5 seconds
|
||||||
|
// after the LAST database modification (not the first).
|
||||||
|
//
|
||||||
|
// Debouncing behavior: If multiple operations happen within 5 seconds, the timer
|
||||||
|
// resets each time, and only one flush occurs after the burst of activity completes.
|
||||||
|
// This prevents excessive writes during rapid issue creation/updates.
|
||||||
|
//
|
||||||
|
// Flush-on-exit guarantee: PersistentPostRun cancels the timer and flushes immediately
|
||||||
|
// before the command exits, ensuring no data is lost even if the timer hasn't fired.
|
||||||
|
//
|
||||||
|
// Thread-safe: Protected by flushMutex. Safe to call from multiple goroutines.
|
||||||
|
// No-op if auto-flush is disabled via --no-auto-flush flag.
|
||||||
func markDirtyAndScheduleFlush() {
|
func markDirtyAndScheduleFlush() {
|
||||||
if !autoFlushEnabled {
|
if !autoFlushEnabled {
|
||||||
return
|
return
|
||||||
@@ -1051,6 +1073,27 @@ func clearAutoFlushState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// flushToJSONL exports dirty issues to JSONL using incremental updates
|
// flushToJSONL exports dirty issues to JSONL using incremental updates
|
||||||
|
// flushToJSONL exports dirty database changes to the JSONL file. Uses incremental
|
||||||
|
// export by default (only exports modified issues), or full export for ID-changing
|
||||||
|
// operations (renumber, resolve-collisions). Invoked by the debounce timer or
|
||||||
|
// immediately on command exit.
|
||||||
|
//
|
||||||
|
// Export modes:
|
||||||
|
// - Incremental (default): Exports only GetDirtyIssues(), merges with existing JSONL
|
||||||
|
// - Full (after renumber): Exports all issues, rebuilds JSONL from scratch
|
||||||
|
//
|
||||||
|
// Error handling: Tracks consecutive failures. After 3+ failures, displays prominent
|
||||||
|
// warning suggesting manual "bd export" to recover. Failure counter resets on success.
|
||||||
|
//
|
||||||
|
// Thread-safety:
|
||||||
|
// - Protected by flushMutex for isDirty/needsFullExport access
|
||||||
|
// - Checks storeActive flag (via storeMutex) to prevent use-after-close
|
||||||
|
// - Safe to call from timer goroutine or main thread
|
||||||
|
//
|
||||||
|
// No-op conditions:
|
||||||
|
// - Store already closed (storeActive=false)
|
||||||
|
// - Database not dirty (isDirty=false)
|
||||||
|
// - No dirty issues found (incremental mode only)
|
||||||
func flushToJSONL() {
|
func flushToJSONL() {
|
||||||
// Check if store is still active (not closed)
|
// Check if store is still active (not closed)
|
||||||
storeMutex.Lock()
|
storeMutex.Lock()
|
||||||
|
|||||||
@@ -152,12 +152,6 @@ func equalStringPtr(a, b *string) bool {
|
|||||||
//
|
//
|
||||||
// Reference score = text mentions + dependency references
|
// Reference score = text mentions + dependency references
|
||||||
func ScoreCollisions(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, allIssues []*types.Issue) error {
|
func ScoreCollisions(ctx context.Context, s *SQLiteStorage, collisions []*CollisionDetail, allIssues []*types.Issue) error {
|
||||||
// Build a map of all issues for quick lookup
|
|
||||||
issueMap := make(map[string]*types.Issue)
|
|
||||||
for _, issue := range allIssues {
|
|
||||||
issueMap[issue.ID] = issue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all dependency records for efficient lookup
|
// Get all dependency records for efficient lookup
|
||||||
allDeps, err := s.GetAllDependencyRecords(ctx)
|
allDeps, err := s.GetAllDependencyRecords(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1149,18 +1149,25 @@ func (s *SQLiteStorage) UpdateIssue(ctx context.Context, id string, updates map[
|
|||||||
|
|
||||||
// UpdateIssueID updates an issue ID and all its text fields in a single transaction
|
// UpdateIssueID updates an issue ID and all its text fields in a single transaction
|
||||||
func (s *SQLiteStorage) UpdateIssueID(ctx context.Context, oldID, newID string, issue *types.Issue, actor string) error {
|
func (s *SQLiteStorage) UpdateIssueID(ctx context.Context, oldID, newID string, issue *types.Issue, actor string) error {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
// Get exclusive connection to ensure PRAGMA applies
|
||||||
|
conn, err := s.db.Conn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get connection: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Disable foreign keys on this specific connection
|
||||||
|
_, err = conn.ExecContext(ctx, `PRAGMA foreign_keys = OFF`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to disable foreign keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// Defer foreign key checks until end of transaction
|
|
||||||
_, err = tx.ExecContext(ctx, `PRAGMA defer_foreign_keys = ON`)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to defer foreign keys: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, `
|
_, err = tx.ExecContext(ctx, `
|
||||||
UPDATE issues
|
UPDATE issues
|
||||||
SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
|
SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
|
||||||
|
|||||||
Reference in New Issue
Block a user