{"record":{"id":"f651bdb3c2da2de3","repo":"gastownhall/beads","slug":"scan-child-status-w","errorCode":null,"errorMessage":"scan child status: %w","messagePattern":"scan child status: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/epic_closure.go","lineNumber":98,"sourceCode":"\t\t\t\tif end > len(allChildIDs) {\n\t\t\t\t\tend = len(allChildIDs)\n\t\t\t\t}\n\t\t\t\tbatch := allChildIDs[start:end]\n\t\t\t\tplaceholders, args := buildSQLInClause(batch)\n\n\t\t\t\tstatusQuery := fmt.Sprintf(\"SELECT id, status FROM %s WHERE id IN (%s)\", table, placeholders)\n\t\t\t\tstatusRows, err := tx.QueryContext(ctx, statusQuery, args...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif isTableNotExistError(err) {\n\t\t\t\t\t\tbreak // wisps table may not exist on pre-migration databases\n\t\t\t\t\t}\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to batch-fetch child statuses from %s: %w\", table, err)\n\t\t\t\t}\n\t\t\t\tfor statusRows.Next() {\n\t\t\t\t\tvar id, status string\n\t\t\t\t\tif err := statusRows.Scan(&id, &status); err != nil {\n\t\t\t\t\t\tstatusRows.Close()\n\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)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/epic_closure.go#L80-L116","documentation":"Rows from the child-status batch query are scanned into two strings (id, status). A Scan mismatch aborts with this error. It means a child row's id or status could not be read as a string, so the child-status map would be incomplete and closure evaluation is halted.","triggerScenarios":"Calling GetEpicsEligibleForClosureInTx when a status row contains a NULL or non-string id/status value that the driver cannot convert.","commonSituations":"Rows with NULL status inserted by other tooling; enum/typed status columns in a divergent schema; corrupted wisps/issues rows.","solutions":["Locate rows with NULL/invalid status in the named table and set them to a valid status.","Align the schema with the current bd migrations.","Remove corrupt rows and re-sync from the primary database.","Retry the closure command after repair."],"exampleFix":"// before\nUPDATE issues SET status = NULL WHERE id = 'bd-1';\n// after\nUPDATE issues SET status = 'open' WHERE id = 'bd-1';","handlingStrategy":"validation","validationCode":"rows, _ := db.Query(\"SELECT id, status FROM issues WHERE id IN (?)\", ids)\nfor rows.Next() {\n    var id, status sql.NullString\n    _ = rows.Scan(&id, &status)\n    if !id.Valid || !status.Valid { return fmt.Errorf(\"child %v has NULL status\", id) }\n}","typeGuard":"func isChildStatusScanErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"scan child status:\")\n}","tryCatchPattern":"epics, err := GetEpicsEligibleForClosureInTx(ctx, tx)\nif err != nil {\n    if isChildStatusScanErr(err) { return backfillStatusesThenRetry(ctx) }\n    return err\n}","preventionTips":["Backfill NULL statuses to a valid default after schema changes.","Validate statuses against bd's allowed values on import.","Avoid external writers to issues/wisps tables.","Run integrity checks after restores."],"tags":["go","sql","scan","data-integrity"],"backgroundTag":"sql-row-scan-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}