diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index 79744632..11f9c88a 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -82,7 +82,7 @@ {"id":"bd-52","title":"Clean up linter errors (914 total issues)","description":"The codebase has 914 linter issues reported by golangci-lint. While many are documented as baseline in LINTING.md, we should clean these up systematically to improve code quality and maintainability.","design":"Break down by linter category, prioritizing high-impact issues:\n1. dupl (7) - Code duplication\n2. goconst (12) - Repeated strings\n3. gocyclo (11) - High complexity functions\n4. revive (78) - Style issues\n5. gosec (102) - Security warnings\n6. errcheck (683) - Unchecked errors (many in tests)","acceptance_criteria":"All linter categories reduced to acceptable levels, with remaining baseline documented in LINTING.md","notes":"Reduced from 56 to 41 issues locally, then to 0 issues.\n\n**Fixed in commits:**\n- c2c7eda: Fixed 15 actual errors (dupl, gosec, revive, staticcheck, unparam)\n- 963181d: Configured exclusions to get to 0 issues locally\n\n**Current status:**\n- ✅ Local: golangci-lint reports 0 issues\n- ❌ CI: Still failing (see bd-65)\n\n**Problem:**\nConfig v2 format or golangci-lint-action@v8 compatibility issue causing CI to fail despite local success.\n\n**Next:** Debug bd-65 to fix CI/local discrepancy","status":"in_progress","priority":2,"issue_type":"epic","created_at":"2025-10-24T01:01:12.997982-07:00","updated_at":"2025-10-24T13:51:54.439577-07:00"} {"id":"bd-53","title":"Fix code duplication in label.go (dupl)","description":"Lines 72-120 duplicate lines 122-170 in cmd/bd/label.go. The add and remove commands have nearly identical structure.","design":"Extract common batch operation logic into a shared helper function that takes the operation type as a parameter.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-24T01:01:36.971666-07:00","updated_at":"2025-10-24T13:51:54.416434-07:00","closed_at":"2025-10-24T12:40:43.046348-07:00","dependencies":[{"issue_id":"bd-53","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.325899-07:00","created_by":"renumber"}]} {"id":"bd-54","title":"Convert repeated strings to constants (goconst)","description":"12 instances of repeated strings that should be constants: \"alice\", \"windows\", \"bd-114\", \"daemon\", \"import\", \"healthy\", \"unhealthy\", \"1.0.0\", \"custom-1\", \"custom-2\"","design":"Create package-level or test-level constants for frequently used test strings and command names.","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-24T01:01:36.9778-07:00","updated_at":"2025-10-24T13:51:54.439751-07:00","dependencies":[{"issue_id":"bd-54","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.326123-07:00","created_by":"renumber"}]} -{"id":"bd-55","title":"Refactor high complexity functions (gocyclo)","description":"11 functions exceed cyclomatic complexity threshold (\u003e30): runDaemonLoop (42), importIssuesCore (71), TestLabelCommands (67), issueDataChanged (39), etc.","design":"Break down complex functions into smaller, testable units. Extract validation, error handling, and business logic into separate functions.","notes":"Refactored issueDataChanged from complexity 39 → 11 by extracting into fieldComparator struct with methods for each comparison type.\n\nRefactored runDaemonLoop from complexity 42 → 7 by extracting:\n- setupDaemonLogger: Logger initialization logic\n- setupDaemonLock: Lock and PID file management\n- startRPCServer: RPC server startup with error handling\n- runGlobalDaemon: Global daemon mode handling\n- createSyncFunc: Sync cycle logic (export, commit, pull, import, push)\n- runEventLoop: Signal handling and main event loop\n\nCode review fixes:\n- Fixed sync overlap: Changed initial sync from `go doSync()` to synchronous `doSync()` to prevent race with ticker\n- Fixed resource cleanup: Replaced `os.Exit(1)` with `return` after acquiring locks to ensure defers run and clean up PID files/locks\n- Added signal.Stop(sigChan) in runEventLoop and runGlobalDaemon to prevent lingering notifications\n- Added server.Stop() in serverErrChan case for consistent cleanup\n\nRefactored TestLabelCommands from complexity 67 → \u003c10 by extracting labelTestHelper with methods:\n- createIssue: Issue creation helper\n- addLabel/addLabels: Label addition helpers\n- removeLabel: Label removal helper\n- getLabels: Label retrieval helper\n- assertLabelCount/assertHasLabel/assertHasLabels/assertNotHasLabel: Assertion helpers\n- assertLabelEvent: Event verification helper\n\nRefactored TestReopenCommand from complexity 37 → \u003c10 by extracting reopenTestHelper with methods:\n- createIssue: Issue creation helper\n- closeIssue/reopenIssue: State transition helpers\n- getIssue: Issue retrieval helper\n- addComment: Comment addition helper\n- assertStatus/assertClosedAtSet/assertClosedAtNil: Status assertion helpers\n- assertCommentEvent: Event verification helper\n\nAll tests pass after refactoring.\n\n**Remaining functions (7):**\n1. TestLibraryIntegration (beads_integration_test.go:14) - complexity 32\n2. TestExportImport (cmd/bd/export_import_test.go:17) - complexity 31\n3. TestListCommand (cmd/bd/list_test.go:15) - complexity 31\n4. tryAutoStartDaemon (cmd/bd/main.go:625) - complexity 34\n5. TestGetEpicsEligibleForClosure (internal/storage/sqlite/epics_test.go:10) - complexity 32\n6. DeleteIssues (internal/storage/sqlite/sqlite.go:1466) - complexity 37\n7. TestCreateIssues (internal/storage/sqlite/sqlite_test.go:195) - complexity 35","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-10-24T01:01:36.989066-07:00","updated_at":"2025-10-25T12:19:23.537919-07:00","dependencies":[{"issue_id":"bd-55","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.323992-07:00","created_by":"renumber"}]} +{"id":"bd-55","title":"Refactor high complexity functions (gocyclo)","description":"11 functions exceed cyclomatic complexity threshold (\u003e30): runDaemonLoop (42), importIssuesCore (71), TestLabelCommands (67), issueDataChanged (39), etc.","design":"Break down complex functions into smaller, testable units. Extract validation, error handling, and business logic into separate functions.","notes":"Refactored issueDataChanged from complexity 39 → 11 by extracting into fieldComparator struct with methods for each comparison type.\n\nRefactored runDaemonLoop from complexity 42 → 7 by extracting:\n- setupDaemonLogger: Logger initialization logic\n- setupDaemonLock: Lock and PID file management\n- startRPCServer: RPC server startup with error handling\n- runGlobalDaemon: Global daemon mode handling\n- createSyncFunc: Sync cycle logic (export, commit, pull, import, push)\n- runEventLoop: Signal handling and main event loop\n\nCode review fixes:\n- Fixed sync overlap: Changed initial sync from `go doSync()` to synchronous `doSync()` to prevent race with ticker\n- Fixed resource cleanup: Replaced `os.Exit(1)` with `return` after acquiring locks to ensure defers run and clean up PID files/locks\n- Added signal.Stop(sigChan) in runEventLoop and runGlobalDaemon to prevent lingering notifications\n- Added server.Stop() in serverErrChan case for consistent cleanup\n\nRefactored TestLabelCommands from complexity 67 → \u003c10 by extracting labelTestHelper with methods:\n- createIssue: Issue creation helper\n- addLabel/addLabels: Label addition helpers\n- removeLabel: Label removal helper\n- getLabels: Label retrieval helper\n- assertLabelCount/assertHasLabel/assertHasLabels/assertNotHasLabel: Assertion helpers\n- assertLabelEvent: Event verification helper\n\nRefactored TestReopenCommand from complexity 37 → \u003c10 by extracting reopenTestHelper with methods:\n- createIssue: Issue creation helper\n- closeIssue/reopenIssue: State transition helpers\n- getIssue: Issue retrieval helper\n- addComment: Comment addition helper\n- assertStatus/assertClosedAtSet/assertClosedAtNil: Status assertion helpers\n- assertCommentEvent: Event verification helper\n\nRefactored tryAutoStartDaemon from complexity 34 → \u003c10 by extracting:\n- debugLog: Centralized debug logging helper\n- isDaemonHealthy: Fast-path health check\n- acquireStartLock: Lock acquisition with wait/retry logic\n- handleStaleLock: Stale lock detection and retry\n- handleExistingSocket: Socket cleanup and validation\n- determineSocketMode: Global vs local daemon logic\n- startDaemonProcess: Process spawning and readiness wait\n- setupDaemonIO: I/O redirection setup\n\nAll tests pass after refactoring.\n\n**Remaining functions (6):**\n1. TestLibraryIntegration (beads_integration_test.go:14) - complexity 32\n2. TestExportImport (cmd/bd/export_import_test.go:17) - complexity 31\n3. TestListCommand (cmd/bd/list_test.go:15) - complexity 31\n4. TestGetEpicsEligibleForClosure (internal/storage/sqlite/epics_test.go:10) - complexity 32\n5. DeleteIssues (internal/storage/sqlite/sqlite.go:1466) - complexity 37\n6. TestCreateIssues (internal/storage/sqlite/sqlite_test.go:195) - complexity 35","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-10-24T01:01:36.989066-07:00","updated_at":"2025-10-25T12:35:32.006358-07:00","dependencies":[{"issue_id":"bd-55","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.323992-07:00","created_by":"renumber"}]} {"id":"bd-56","title":"Fix revive style issues (78 issues)","description":"Style violations: unused parameters (many cmd/args in cobra commands), missing exported comments, stuttering names (SQLiteStorage), indent-error-flow issues.","design":"Rename unused params to _, add godoc comments to exported types, fix stuttering names, simplify control flow.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-24T01:01:36.99984-07:00","updated_at":"2025-10-24T13:51:54.417341-07:00","dependencies":[{"issue_id":"bd-56","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.322412-07:00","created_by":"renumber"}]} {"id":"bd-57","title":"Address gosec security warnings (102 issues)","description":"Security linter warnings: file permissions (0755 should be 0750), G304 file inclusion via variable, G204 subprocess launches. Many are false positives but should be reviewed.","design":"Review each gosec warning. Add exclusions for legitimate cases to .golangci.yml. Fix real security issues (overly permissive file modes).","status":"open","priority":2,"issue_type":"task","created_at":"2025-10-24T01:01:37.0139-07:00","updated_at":"2025-10-24T13:51:54.417632-07:00","dependencies":[{"issue_id":"bd-57","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.324202-07:00","created_by":"renumber"}]} {"id":"bd-58","title":"Handle unchecked errors (errcheck - 683 issues)","description":"683 unchecked error returns, mostly in tests (Close, Rollback, RemoveAll). Many already excluded in config but still showing up.","design":"Review .golangci.yml exclude-rules. Most defer Close/Rollback errors in tests can be ignored. Add systematic exclusions or explicit _ = assignments where appropriate.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-24T01:01:37.018404-07:00","updated_at":"2025-10-24T13:51:54.41793-07:00","dependencies":[{"issue_id":"bd-58","depends_on_id":"bd-52","type":"parent-child","created_at":"2025-10-24T13:17:40.324423-07:00","created_by":"renumber"}]} diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index ea899fa8..a168102e 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -1474,196 +1474,210 @@ func (s *SQLiteStorage) DeleteIssues(ctx context.Context, ids []string, cascade } defer func() { _ = tx.Rollback() }() + idSet := buildIDSet(ids) result := &DeleteIssuesResult{} - // Build ID set for efficient lookup + expandedIDs, err := s.resolveDeleteSet(ctx, tx, ids, idSet, cascade, force, result) + if err != nil { + return nil, err + } + + inClause, args := buildSQLInClause(expandedIDs) + if err := s.populateDeleteStats(ctx, tx, inClause, args, result); err != nil { + return nil, err + } + + if dryRun { + return result, nil + } + + if err := s.executeDelete(ctx, tx, inClause, args, result); err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + if err := s.SyncAllCounters(ctx); err != nil { + return nil, fmt.Errorf("failed to sync counters after deletion: %w", err) + } + + return result, nil +} + +func buildIDSet(ids []string) map[string]bool { idSet := make(map[string]bool, len(ids)) for _, id := range ids { idSet[id] = true } + return idSet +} - // If cascade mode, find all dependent issues recursively +func (s *SQLiteStorage) resolveDeleteSet(ctx context.Context, tx *sql.Tx, ids []string, idSet map[string]bool, cascade bool, force bool, result *DeleteIssuesResult) ([]string, error) { if cascade { - allToDelete, err := s.findAllDependentsRecursive(ctx, tx, ids) - if err != nil { - return nil, fmt.Errorf("failed to find dependents: %w", err) + return s.expandWithDependents(ctx, tx, ids, idSet) + } + if !force { + return ids, s.validateNoDependents(ctx, tx, ids, idSet, result) + } + return ids, s.trackOrphanedIssues(ctx, tx, ids, idSet, result) +} + +func (s *SQLiteStorage) expandWithDependents(ctx context.Context, tx *sql.Tx, ids []string, idSet map[string]bool) ([]string, error) { + allToDelete, err := s.findAllDependentsRecursive(ctx, tx, ids) + if err != nil { + return nil, fmt.Errorf("failed to find dependents: %w", err) + } + expandedIDs := make([]string, 0, len(allToDelete)) + for id := range allToDelete { + expandedIDs = append(expandedIDs, id) + } + return expandedIDs, nil +} + +func (s *SQLiteStorage) validateNoDependents(ctx context.Context, tx *sql.Tx, ids []string, idSet map[string]bool, result *DeleteIssuesResult) error { + for _, id := range ids { + if err := s.checkSingleIssueValidation(ctx, tx, id, idSet, result); err != nil { + return err } - // Update ids to include all dependents - for id := range allToDelete { - idSet[id] = true + } + return nil +} + +func (s *SQLiteStorage) checkSingleIssueValidation(ctx context.Context, tx *sql.Tx, id string, idSet map[string]bool, result *DeleteIssuesResult) error { + var depCount int + err := tx.QueryRowContext(ctx, + `SELECT COUNT(*) FROM dependencies WHERE depends_on_id = ?`, id).Scan(&depCount) + if err != nil { + return fmt.Errorf("failed to check dependents for %s: %w", id, err) + } + if depCount == 0 { + return nil + } + + rows, err := tx.QueryContext(ctx, + `SELECT issue_id FROM dependencies WHERE depends_on_id = ?`, id) + if err != nil { + return fmt.Errorf("failed to get dependents for %s: %w", id, err) + } + defer rows.Close() + + hasExternal := false + for rows.Next() { + var depID string + if err := rows.Scan(&depID); err != nil { + return fmt.Errorf("failed to scan dependent: %w", err) } - ids = make([]string, 0, len(idSet)) - for id := range idSet { - ids = append(ids, id) - } - } else if !force { - // Check if any issue has dependents not in the deletion set - for _, id := range ids { - var depCount int - err := tx.QueryRowContext(ctx, - `SELECT COUNT(*) FROM dependencies WHERE depends_on_id = ?`, id).Scan(&depCount) - if err != nil { - return nil, fmt.Errorf("failed to check dependents for %s: %w", id, err) - } - if depCount > 0 { - // Check if all dependents are in deletion set - rows, err := tx.QueryContext(ctx, - `SELECT issue_id FROM dependencies WHERE depends_on_id = ?`, id) - if err != nil { - return nil, fmt.Errorf("failed to get dependents for %s: %w", id, err) - } - hasExternalDependents := false - for rows.Next() { - var depID string - if err := rows.Scan(&depID); err != nil { - _ = rows.Close() - return nil, fmt.Errorf("failed to scan dependent: %w", err) - } - if !idSet[depID] { - hasExternalDependents = true - result.OrphanedIssues = append(result.OrphanedIssues, depID) - } - } - _ = rows.Close() - if hasExternalDependents { - return nil, fmt.Errorf("issue %s has dependents not in deletion set; use --cascade to delete them or --force to orphan them", id) - } - } - } - } else { - // Force mode: track orphaned issues (deduplicate) - orphanSet := make(map[string]bool) - for _, id := range ids { - rows, err := tx.QueryContext(ctx, - `SELECT issue_id FROM dependencies WHERE depends_on_id = ?`, id) - if err != nil { - return nil, fmt.Errorf("failed to get dependents for %s: %w", id, err) - } - for rows.Next() { - var depID string - if err := rows.Scan(&depID); err != nil { - _ = rows.Close() - return nil, fmt.Errorf("failed to scan dependent: %w", err) - } - if !idSet[depID] { - orphanSet[depID] = true - } - } - if err := rows.Err(); err != nil { - _ = rows.Close() - return nil, fmt.Errorf("failed to iterate dependents: %w", err) - } - _ = rows.Close() - } - // Convert set to slice - for orphanID := range orphanSet { - result.OrphanedIssues = append(result.OrphanedIssues, orphanID) + if !idSet[depID] { + hasExternal = true + result.OrphanedIssues = append(result.OrphanedIssues, depID) } } - // Build placeholders for SQL IN clause + if err := rows.Err(); err != nil { + return fmt.Errorf("failed to iterate dependents for %s: %w", id, err) + } + + if hasExternal { + return fmt.Errorf("issue %s has dependents not in deletion set; use --cascade to delete them or --force to orphan them", id) + } + return nil +} + +func (s *SQLiteStorage) trackOrphanedIssues(ctx context.Context, tx *sql.Tx, ids []string, idSet map[string]bool, result *DeleteIssuesResult) error { + orphanSet := make(map[string]bool) + for _, id := range ids { + if err := s.collectOrphansForID(ctx, tx, id, idSet, orphanSet); err != nil { + return err + } + } + for orphanID := range orphanSet { + result.OrphanedIssues = append(result.OrphanedIssues, orphanID) + } + return nil +} + +func (s *SQLiteStorage) collectOrphansForID(ctx context.Context, tx *sql.Tx, id string, idSet map[string]bool, orphanSet map[string]bool) error { + rows, err := tx.QueryContext(ctx, + `SELECT issue_id FROM dependencies WHERE depends_on_id = ?`, id) + if err != nil { + return fmt.Errorf("failed to get dependents for %s: %w", id, err) + } + defer rows.Close() + + for rows.Next() { + var depID string + if err := rows.Scan(&depID); err != nil { + return fmt.Errorf("failed to scan dependent: %w", err) + } + if !idSet[depID] { + orphanSet[depID] = true + } + } + return rows.Err() +} + +func buildSQLInClause(ids []string) (string, []interface{}) { placeholders := make([]string, len(ids)) args := make([]interface{}, len(ids)) for i, id := range ids { placeholders[i] = "?" args[i] = id } - inClause := strings.Join(placeholders, ",") + return strings.Join(placeholders, ","), args +} - // Count dependencies - var depCount int - err = tx.QueryRowContext(ctx, - fmt.Sprintf(`SELECT COUNT(*) FROM dependencies WHERE issue_id IN (%s) OR depends_on_id IN (%s)`, - inClause, inClause), - append(args, args...)...).Scan(&depCount) - if err != nil { - return nil, fmt.Errorf("failed to count dependencies: %w", err) - } - result.DependenciesCount = depCount - - // Count labels - var labelCount int - err = tx.QueryRowContext(ctx, - fmt.Sprintf(`SELECT COUNT(*) FROM labels WHERE issue_id IN (%s)`, inClause), - args...).Scan(&labelCount) - if err != nil { - return nil, fmt.Errorf("failed to count labels: %w", err) - } - result.LabelsCount = labelCount - - // Count events - var eventCount int - err = tx.QueryRowContext(ctx, - fmt.Sprintf(`SELECT COUNT(*) FROM events WHERE issue_id IN (%s)`, inClause), - args...).Scan(&eventCount) - if err != nil { - return nil, fmt.Errorf("failed to count events: %w", err) - } - result.EventsCount = eventCount - result.DeletedCount = len(ids) - - // If dry-run, return statistics without deleting - if dryRun { - return result, nil +func (s *SQLiteStorage) populateDeleteStats(ctx context.Context, tx *sql.Tx, inClause string, args []interface{}, result *DeleteIssuesResult) error { + counts := []struct { + query string + dest *int + }{ + {fmt.Sprintf(`SELECT COUNT(*) FROM dependencies WHERE issue_id IN (%s) OR depends_on_id IN (%s)`, inClause, inClause), &result.DependenciesCount}, + {fmt.Sprintf(`SELECT COUNT(*) FROM labels WHERE issue_id IN (%s)`, inClause), &result.LabelsCount}, + {fmt.Sprintf(`SELECT COUNT(*) FROM events WHERE issue_id IN (%s)`, inClause), &result.EventsCount}, } - // Delete dependencies - _, err = tx.ExecContext(ctx, - fmt.Sprintf(`DELETE FROM dependencies WHERE issue_id IN (%s) OR depends_on_id IN (%s)`, - inClause, inClause), - append(args, args...)...) - if err != nil { - return nil, fmt.Errorf("failed to delete dependencies: %w", err) + for _, c := range counts { + queryArgs := args + if c.dest == &result.DependenciesCount { + queryArgs = append(args, args...) + } + if err := tx.QueryRowContext(ctx, c.query, queryArgs...).Scan(c.dest); err != nil { + return fmt.Errorf("failed to count: %w", err) + } } - // Delete labels - _, err = tx.ExecContext(ctx, - fmt.Sprintf(`DELETE FROM labels WHERE issue_id IN (%s)`, inClause), - args...) - if err != nil { - return nil, fmt.Errorf("failed to delete labels: %w", err) + result.DeletedCount = len(args) + return nil +} + +func (s *SQLiteStorage) executeDelete(ctx context.Context, tx *sql.Tx, inClause string, args []interface{}, result *DeleteIssuesResult) error { + deletes := []struct { + query string + args []interface{} + }{ + {fmt.Sprintf(`DELETE FROM dependencies WHERE issue_id IN (%s) OR depends_on_id IN (%s)`, inClause, inClause), append(args, args...)}, + {fmt.Sprintf(`DELETE FROM labels WHERE issue_id IN (%s)`, inClause), args}, + {fmt.Sprintf(`DELETE FROM events WHERE issue_id IN (%s)`, inClause), args}, + {fmt.Sprintf(`DELETE FROM dirty_issues WHERE issue_id IN (%s)`, inClause), args}, + {fmt.Sprintf(`DELETE FROM issues WHERE id IN (%s)`, inClause), args}, } - // Delete events - _, err = tx.ExecContext(ctx, - fmt.Sprintf(`DELETE FROM events WHERE issue_id IN (%s)`, inClause), - args...) - if err != nil { - return nil, fmt.Errorf("failed to delete events: %w", err) + for i, d := range deletes { + execResult, err := tx.ExecContext(ctx, d.query, d.args...) + if err != nil { + return fmt.Errorf("failed to delete: %w", err) + } + if i == len(deletes)-1 { + rowsAffected, err := execResult.RowsAffected() + if err != nil { + return fmt.Errorf("failed to check rows affected: %w", err) + } + result.DeletedCount = int(rowsAffected) + } } - - // Delete from dirty_issues - _, err = tx.ExecContext(ctx, - fmt.Sprintf(`DELETE FROM dirty_issues WHERE issue_id IN (%s)`, inClause), - args...) - if err != nil { - return nil, fmt.Errorf("failed to delete dirty markers: %w", err) - } - - // Delete the issues themselves - deleteResult, err := tx.ExecContext(ctx, - fmt.Sprintf(`DELETE FROM issues WHERE id IN (%s)`, inClause), - args...) - if err != nil { - return nil, fmt.Errorf("failed to delete issues: %w", err) - } - - rowsAffected, err := deleteResult.RowsAffected() - if err != nil { - return nil, fmt.Errorf("failed to check rows affected: %w", err) - } - result.DeletedCount = int(rowsAffected) - - if err := tx.Commit(); err != nil { - return nil, fmt.Errorf("failed to commit transaction: %w", err) - } - - // Sync counters after deletion to keep them accurate - if err := s.SyncAllCounters(ctx); err != nil { - return nil, fmt.Errorf("failed to sync counters after deletion: %w", err) - } - - return result, nil + return nil } // findAllDependentsRecursive finds all issues that depend on the given issues, recursively