{"record":{"id":"ff9f3cd7832a68b1","repo":"gastownhall/beads","slug":"db-issuesqlrepository-getepicseligibleforclosure","errorCode":null,"errorMessage":"db: IssueSQLRepository.GetEpicsEligibleForClosure: %w","messagePattern":"db: IssueSQLRepository\\.GetEpicsEligibleForClosure: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":1209,"sourceCode":"\tevents, err := issueops.GetEventsInTx(ctx, r.runner, id, limit)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.IterEvents: %w\", err)\n\t}\n\treturn storage.NewSliceIter(events), nil\n}\n\nfunc (r *issueSQLRepositoryImpl) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {\n\tout, err := issueops.GetStaleIssuesInTx(ctx, r.runner, filter)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.GetStaleIssues: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {\n\tout, err := issueops.GetEpicsEligibleForClosureInTx(ctx, r.runner)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db: IssueSQLRepository.GetEpicsEligibleForClosure: %w\", err)\n\t}\n\treturn out, nil\n}\n\nfunc (r *issueSQLRepositoryImpl) UnclaimIssue(ctx context.Context, id, actor string, force bool) error {\n\tif err := issueops.UnclaimIssueInTx(ctx, r.runner, id, actor, force); err != nil {\n\t\treturn fmt.Errorf(\"db: IssueSQLRepository.UnclaimIssue: %w\", err)\n\t}\n\treturn nil\n}\n\n// UnclaimIssueIfAssignee runs the classic compare-and-swap release against this\n// runner. Like UnclaimIssue it takes no IssueTableOpts: issueops routes the\n// write to the issues or wisps tables from the row itself, so a wisp's claim is\n// released against the wisp tables on both backends. The mismatch verdict\n// (storage.ErrAssigneeMismatch, nothing written) is produced by the shared\n// helper, not restated here.\nfunc (r *issueSQLRepositoryImpl) UnclaimIssueIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {","sourceCodeStart":1191,"sourceCodeEnd":1227,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L1191-L1227","documentation":"Wraps a failure from issueops.GetEpicsEligibleForClosureInTx, which scans open epic IDs and checks dependency/child status to decide which epics may close. The wrapper only adds repository context — the real cause (SQL error, scan error) is in the wrapped chain. This runs during epic auto-closure flows.","triggerScenarios":"Any error inside GetEpicsEligibleForClosureInTx: the initial 'SELECT id FROM issues WHERE issue_type='epic'' scan fails, an IN-clause query over child issues fails, or optional blocked-table probing errors.","commonSituations":"Schema drift (issues table missing expected columns), table-not-exist on a partial migration, connection failure during the multi-query closure sweep, or query timeout with large epic sets.","solutions":["Inspect the wrapped cause in the error chain for the exact SQL failure.","Verify issues/dependencies tables exist with current schema (run migrations / bd doctor).","Retry on transient connection errors; the operation is read-only and safe to re-run.","Reduce scope/tune timeouts if the sweep times out with many epics."],"exampleFix":"// before\nout, err := repo.GetEpicsEligibleForClosure(ctx)\nif err != nil { log.Fatal(err) }\n// after\nout, err := repo.GetEpicsEligibleForClosure(ctx)\nif err != nil {\n    log.Printf(\"epic closure sweep skipped: %v\", err) // read-only, safe to retry later\n    return\n}","handlingStrategy":"retry","validationCode":"// cheap pre-check: are there open epics at all?\nvar n int\n_ = db.QueryRow(\"SELECT COUNT(*) FROM issues WHERE issue_type='epic' AND status != 'closed'\").Scan(&n)\nif n == 0 { return } // skip sweep","typeGuard":null,"tryCatchPattern":"out, err := repo.GetEpicsEligibleForClosure(ctx)\nif err != nil {\n    if isTransientDBErr(err) { scheduleRetry(err); return }\n    log.Printf(\"epic closure sweep failed: %v\", err)\n    return\n}","preventionTips":["Treat the sweep as idempotent/read-only and retry on failure.","Run migrations before enabling epic auto-closure.","Avoid overlapping sweeps with a scheduler lock.","Alert on the wrapped cause, not just the wrapper message."],"tags":["database","epic","query"],"backgroundTag":"db-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}