gastownhall/beads · error

db: IssueSQLRepository.GetEpicsEligibleForClosure: %w

Error message

db: IssueSQLRepository.GetEpicsEligibleForClosure: %w

What it means

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.

Source

Thrown at internal/storage/domain/db/issue.go:1209

	events, err := issueops.GetEventsInTx(ctx, r.runner, id, limit)
	if err != nil {
		return nil, fmt.Errorf("db: IssueSQLRepository.IterEvents: %w", err)
	}
	return storage.NewSliceIter(events), nil
}

func (r *issueSQLRepositoryImpl) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {
	out, err := issueops.GetStaleIssuesInTx(ctx, r.runner, filter)
	if err != nil {
		return nil, fmt.Errorf("db: IssueSQLRepository.GetStaleIssues: %w", err)
	}
	return out, nil
}

func (r *issueSQLRepositoryImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {
	out, err := issueops.GetEpicsEligibleForClosureInTx(ctx, r.runner)
	if err != nil {
		return nil, fmt.Errorf("db: IssueSQLRepository.GetEpicsEligibleForClosure: %w", err)
	}
	return out, nil
}

func (r *issueSQLRepositoryImpl) UnclaimIssue(ctx context.Context, id, actor string, force bool) error {
	if err := issueops.UnclaimIssueInTx(ctx, r.runner, id, actor, force); err != nil {
		return fmt.Errorf("db: IssueSQLRepository.UnclaimIssue: %w", err)
	}
	return nil
}

// UnclaimIssueIfAssignee runs the classic compare-and-swap release against this
// runner. Like UnclaimIssue it takes no IssueTableOpts: issueops routes the
// write to the issues or wisps tables from the row itself, so a wisp's claim is
// released against the wisp tables on both backends. The mismatch verdict
// (storage.ErrAssigneeMismatch, nothing written) is produced by the shared
// helper, not restated here.
func (r *issueSQLRepositoryImpl) UnclaimIssueIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause in the error chain for the exact SQL failure.
  2. Verify issues/dependencies tables exist with current schema (run migrations / bd doctor).
  3. Retry on transient connection errors; the operation is read-only and safe to re-run.
  4. Reduce scope/tune timeouts if the sweep times out with many epics.

Example fix

// before
out, err := repo.GetEpicsEligibleForClosure(ctx)
if err != nil { log.Fatal(err) }
// after
out, err := repo.GetEpicsEligibleForClosure(ctx)
if err != nil {
    log.Printf("epic closure sweep skipped: %v", err) // read-only, safe to retry later
    return
}
Defensive patterns

Strategy: retry

Validate before calling

// cheap pre-check: are there open epics at all?
var n int
_ = db.QueryRow("SELECT COUNT(*) FROM issues WHERE issue_type='epic' AND status != 'closed'").Scan(&n)
if n == 0 { return } // skip sweep

Try / catch

out, err := repo.GetEpicsEligibleForClosure(ctx)
if err != nil {
    if isTransientDBErr(err) { scheduleRetry(err); return }
    log.Printf("epic closure sweep failed: %v", err)
    return
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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