{"record":{"id":"b83b4b01b86484de","repo":"gastownhall/beads","slug":"failed-to-batch-fetch-epic-issues-w","errorCode":null,"errorMessage":"failed to batch-fetch epic issues: %w","messagePattern":"failed to batch-fetch epic issues: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/epic_closure.go","lineNumber":116,"sourceCode":"\t\t\t\t\t\treturn nil, fmt.Errorf(\"scan child status: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\tchildStatusMap[id] = status\n\t\t\t\t}\n\t\t\t\tstatusRows.Close()\n\t\t\t}\n\t\t}\n\t}\n\n\t// Step 4: Batch-fetch all epic issues\n\tepicsWithChildren := make([]string, 0)\n\tfor _, epicID := range epicIDs {\n\t\tif len(epicChildMap[epicID]) > 0 {\n\t\t\tepicsWithChildren = append(epicsWithChildren, epicID)\n\t\t}\n\t}\n\tepicIssues, err := GetIssuesByIDsInTx(ctx, tx, epicsWithChildren, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to batch-fetch epic issues: %w\", err)\n\t}\n\tepicIssueMap := make(map[string]*types.Issue, len(epicIssues))\n\tfor _, issue := range epicIssues {\n\t\tepicIssueMap[issue.ID] = issue\n\t}\n\n\t// Step 5: Build results from cached data\n\tvar results []*types.EpicStatus\n\tfor _, epicID := range epicIDs {\n\t\tchildren := epicChildMap[epicID]\n\t\tif len(children) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tissue, ok := epicIssueMap[epicID]\n\t\tif !ok || issue == nil {\n\t\t\tcontinue\n\t\t}","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/epic_closure.go#L98-L134","documentation":"After mapping epics to children, the function bulk-loads the epic issues themselves via GetIssuesByIDsInTx. A failure there is wrapped as 'failed to batch-fetch epic issues'. It means the final step of building epicIssueMap failed, so closure decisions cannot include full issue details.","triggerScenarios":"Calling GetEpicsEligibleForClosureInTx when GetIssuesByIDsInTx fails for the collected epic IDs — internal query error, connection drop, or the issues table missing expected columns.","commonSituations":"The same connection dying mid-function after earlier queries succeeded; schema drift in the issues table; an internal bug/regression in GetIssuesByIDsInTx on unusual ID sets (e.g. empty list edge cases).","solutions":["Retry the command; the wrapped cause from GetIssuesByIDsInTx names the real failure.","Check DB connectivity and server logs for mid-query disconnects.","Run migrations if the wrapped error mentions unknown tables/columns.","Upgrade bd if the wrapped error indicates an internal query bug."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if err := db.PingContext(ctx); err != nil { return fmt.Errorf(\"db unavailable: %w\", err) }\nif len(epicIDs) == 0 { return nil } // avoid odd empty-batch paths","typeGuard":"func isEpicBatchFetchErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"failed to batch-fetch epic issues:\")\n}","tryCatchPattern":"epics, err := GetEpicsEligibleForClosureInTx(ctx, tx)\nif err != nil {\n    if isEpicBatchFetchErr(err) && isTransient(err) {\n        return retryWithBackoff(ctx, 3)\n    }\n    return err\n}","preventionTips":["Use a single healthy transaction/connection for the whole closure run.","Retry transient failures with backoff.","Keep schema in sync with the bd version.","Watch for mid-query disconnects in server logs."],"tags":["go","sql","storage","query"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}