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

  1. Check for NULL or malformed `id` values in the issues table (`SELECT id FROM issues WHERE issue_type='epic'`).
  2. Fix or remove rows with NULL/invalid ids.
  3. Ensure the storage driver version matches what bd expects.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/b5ddb18d34864b96. Report an issue: GitHub.