gastownhall/beads · error
scan epic id: %w
Error message
scan epic id: %w
What it means
While iterating the epic-ID result rows, each row is scanned into a single string. If Scan fails — the row shape does not match one string, or the driver returns a type it cannot convert — the function closes the rows and returns this error. It indicates malformed or unexpected data in the epic result set.
Source
Thrown at internal/storage/issueops/epic_closure.go:27
// GetEpicsEligibleForClosureInTx returns epics whose children are all closed.
// nolint:gosec // G201: table names are hardcoded, placeholders contain only ? markers
func GetEpicsEligibleForClosureInTx(ctx context.Context, tx DBTX) ([]*types.EpicStatus, error) {
// Step 1: Get open epic IDs (single-table scan)
epicRows, err := tx.QueryContext(ctx, `
SELECT id FROM issues
WHERE issue_type = 'epic'
AND status != 'closed'
`)
if err != nil {
return nil, fmt.Errorf("failed to get epics: %w", err)
}
var epicIDs []string
for epicRows.Next() {
var id string
if err := epicRows.Scan(&id); err != nil {
epicRows.Close()
return nil, fmt.Errorf("scan epic id: %w", err)
}
epicIDs = append(epicIDs, id)
}
epicRows.Close()
if len(epicIDs) == 0 {
return nil, nil
}
// Step 2: Get parent-child dependencies from both tables (bd-w2w)
// Wisp children store their parent-child deps in wisp_dependencies,
// so we must check both tables to find all children of an epic.
epicChildMap := make(map[string][]string)
epicSet := make(map[string]bool, len(epicIDs))
for _, id := range epicIDs {
epicSet[id] = true
}
for _, depTable := range []string{"dependencies", "wisp_dependencies"} {View on GitHub (pinned to 71377f2769)
Solutions
- Check for NULL or malformed `id` values in the issues table (`SELECT id FROM issues WHERE issue_type='epic'`).
- Fix or remove rows with NULL/invalid ids.
- Ensure the storage driver version matches what bd expects.
- Retry after repairing the data.
Example fix
// before (raw SQL repair) SELECT id FROM issues WHERE issue_type='epic'; // after SELECT id FROM issues WHERE issue_type='epic' AND id IS NOT NULL AND id != '';
Defensive patterns
Strategy: validation
Validate before calling
rows, _ := db.Query("SELECT id FROM issues WHERE issue_type='epic' AND status != 'closed'")
for rows.Next() {
var id sql.NullString
_ = rows.Scan(&id)
if !id.Valid || id.String == "" { return fmt.Errorf("invalid epic id row") }
} Type guard
func isEpicScanErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "scan epic id:")
} Try / catch
epics, err := GetEpicsEligibleForClosureInTx(ctx, tx)
if err != nil {
if isEpicScanErr(err) { return repairIssueRowsThenRetry(ctx) }
return err
} Prevention
- Enforce NOT NULL on issue ids and rely on bd for inserts.
- Validate imported data before loading it into the database.
- Pin bd and driver versions together.
- Audit issues rows after external tooling touches the DB.
When it happens
Trigger: Calling GetEpicsEligibleForClosureInTx when a row returned for the epic query cannot be scanned into a string — e.g. a NULL id, or a driver returning an unexpected column type.
Common situations: Manually inserted rows with NULL ids; driver/version mismatches producing different column types; corrupted rows in the issues table.
Related errors
- scan parent-child dep from %s: %w
- scan child status: %w
- failed to scan dolt_status: %w
- scan dirty config key: %w
- db: ChildCounterSQLRepository.NextChildID: scan: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b5ddb18d34864b96.
Report an issue: GitHub.