gastownhall/beads · error
failed to get rows affected: %w
Error message
failed to get rows affected: %w
What it means
After the UPDATE, UnclaimIssueInTx calls result.RowsAffected() and wraps any driver error here. The library needs the affected-row count to detect a lost optimistic-lock race (0 rows), so a failure here means the driver could not report it and the unclaim outcome is indeterminate.
Source
Thrown at internal/storage/issueops/unclaim.go:91
// oldIssue.RowVersion detects a claim that changed hands (or was released,
// or closed) between that read and this write exactly as precisely as the
// old `assignee = <actor>` predicate did, without embedding a
// spelling-sensitive string comparison in SQL. force does not exempt this
// check: force only widens WHO may unclaim, not whether the row is still
// the one we read.
result, err := tx.ExecContext(ctx, fmt.Sprintf(`
UPDATE %s
SET assignee = '', status = 'open', updated_at = ?,
started_at = NULL, row_lock = ?
WHERE id = ? AND status IN ('open', 'in_progress') AND row_lock = ?
`, issueTable), now, freshRowLock(), id, oldIssue.RowVersion)
if err != nil {
return fmt.Errorf("failed to unclaim issue: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
// The pre-checks passed, so a 0-row result means the row changed
// underneath us: re-read to disambiguate an ownership change from a
// status change. actorMatches, not verbatim, mirrors the precheck above.
current, gerr := GetIssueInTx(ctx, tx, id)
if gerr != nil {
return fmt.Errorf("failed to unclaim issue %s: no matching row", id)
}
if !force && !actorMatches(current.Assignee, actor) {
return fmt.Errorf("%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)",
storage.ErrNotOwner, id, current.Assignee)
}
return fmt.Errorf("failed to unclaim issue %s: no matching row", id)
}
return finishUnclaimInTx(ctx, tx, eventTable, id, actor, oldIssue)View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped driver error; treat as transient and retry the whole unclaim if connection-related.
- Use the supported driver behind the storage driver interface rather than a custom backend.
- Check driver version compatibility and upgrade if RowsAffected is known-buggy.
- Verify the connection/transaction is still alive at the point of the call.
Example fix
// before customStore := NewStore(myOddDriver) // driver lacks RowsAffected support // after store := NewStore(supportedDoltDriver) // standard driver via storage boundary
Defensive patterns
Strategy: retry
Try / catch
if err := store.ReleaseIssue(ctx, id); err != nil {
if strings.Contains(err.Error(), "rows affected") {
// driver-level failure: retry with fresh transaction or switch driver
}
} Prevention
- Stick to supported storage drivers via the driver interface boundary.
- Retry whole transactions on transient driver failures.
- Keep driver and database versions current and tested together.
When it happens
Trigger: Driver failing on RowsAffected() after the UPDATE: unsupported driver capability, connection reset, or driver bug mid-transaction.
Common situations: Custom/misbehaving storage driver lacking RowsAffected support; connection dropped between UPDATE and RowsAffected; backend not exposing affected-row counts.
Related errors
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
- failed to begin transaction: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/3d89623364d6e91e.
Report an issue: GitHub.