gastownhall/beads · error

db: Update %s: affected by status change: %w

Error message

db: Update %s: affected by status change: %w

What it means

This error wraps a failure while computing which issues are affected by a status change during Update. A status change can alter blocking relationships, so the storage layer first queries the affected set (via issueops.AffectedByStatusChangeForWispInTx or AffectedByStatusChangeInTx) inside the same transaction before recomputing is_blocked. If that lookup fails, the entire Update transaction is aborted with this wrapped error.

Source

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

		return err
	}

	if statusChanging {
		newStatus := coerceStatus(updates["status"])
		oldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned
		newActive := newStatus != types.StatusClosed && newStatus != types.StatusPinned
		if oldActive != newActive {
			var (
				affectedIssues, affectedWisps []string
				aerr                          error
			)
			if opts.UseWispsTable {
				affectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeForWispInTx(ctx, r.runner, id)
			} else {
				affectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeInTx(ctx, r.runner, id)
			}
			if aerr != nil {
				return fmt.Errorf("db: Update %s: affected by status change: %w", id, aerr)
			}
			if err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {
				return fmt.Errorf("db: Update %s: recompute is_blocked: %w", id, err)
			}
		}
	}
	// Snapshot only after all derived blocked-state maintenance has completed.
	// The no-op early returns above wrote nothing and journal nothing.
	return issueops.RecordEventInTx(ctx, r.runner, issueops.EventUpdate, id, actor)
}

// CompareAndSetMetadataKey runs the SHARED compare-and-set body, unwrapped.
//
// It is the whole of this leg's implementation, and that is the point: the two
// store backends wrap the same function in their own transaction, so the third
// leg is a wrapper check rather than an independent vote — which is what the
// conformance contract's header says.
//

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause (%w) for the underlying SQL/driver error and fix that first.
  2. Retry the update once the database is reachable; the transaction rolled back so state is consistent.
  3. Verify the issues/wisps and dependency tables match expected migrations.
  4. Check UseWispsTable routing — ensure the correct table variant exists for the store.

Example fix

// before
err := store.Update(ctx, id, opts) // opaque failure mid-status-change
// after
if err := store.Update(ctx, id, opts); err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        err = store.Update(ctx, id, opts) // safe: transactional, rolled back
    }
}
Defensive patterns

Strategy: retry

Validate before calling

if _, err := store.Get(ctx, id, nil); err != nil { return err }
if err := ctx.Err(); err != nil { return err }

Try / catch

if err := store.Update(ctx, id, opts); err != nil {
    var transient = errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF)
    if transient { err = store.Update(ctx, id, opts) }
}

Prevention

When it happens

Trigger: Calling Update on an issue with a status-changing opts while AffectedByStatusChange(InTx/ForWispInTx) errors: transaction aborted, connection dropped, lock-wait timeout, or SQL failure in the dependent lookup.

Common situations: Connection timeouts or server restarts mid-transaction during 'bd update' of a status field; schema drift in dependency tables; concurrent writers causing lock contention; wisp-table routing errors when UseWispsTable is true.

Related errors


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