{"record":{"id":"516fd91da3f51160","repo":"gastownhall/beads","slug":"db-update-s-affected-by-status-change-w","errorCode":null,"errorMessage":"db: Update %s: affected by status change: %w","messagePattern":"db: Update (.+?): affected by status change: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue.go","lineNumber":321,"sourceCode":"\t\treturn err\n\t}\n\n\tif statusChanging {\n\t\tnewStatus := coerceStatus(updates[\"status\"])\n\t\toldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned\n\t\tnewActive := newStatus != types.StatusClosed && newStatus != types.StatusPinned\n\t\tif oldActive != newActive {\n\t\t\tvar (\n\t\t\t\taffectedIssues, affectedWisps []string\n\t\t\t\taerr                          error\n\t\t\t)\n\t\t\tif opts.UseWispsTable {\n\t\t\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeForWispInTx(ctx, r.runner, id)\n\t\t\t} else {\n\t\t\t\taffectedIssues, affectedWisps, aerr = issueops.AffectedByStatusChangeInTx(ctx, r.runner, id)\n\t\t\t}\n\t\t\tif aerr != nil {\n\t\t\t\treturn fmt.Errorf(\"db: Update %s: affected by status change: %w\", id, aerr)\n\t\t\t}\n\t\t\tif err := issueops.RecomputeIsBlockedInTx(ctx, r.runner, affectedIssues, affectedWisps); err != nil {\n\t\t\t\treturn fmt.Errorf(\"db: Update %s: recompute is_blocked: %w\", id, err)\n\t\t\t}\n\t\t}\n\t}\n\t// Snapshot only after all derived blocked-state maintenance has completed.\n\t// The no-op early returns above wrote nothing and journal nothing.\n\treturn issueops.RecordEventInTx(ctx, r.runner, issueops.EventUpdate, id, actor)\n}\n\n// CompareAndSetMetadataKey runs the SHARED compare-and-set body, unwrapped.\n//\n// It is the whole of this leg's implementation, and that is the point: the two\n// store backends wrap the same function in their own transaction, so the third\n// leg is a wrapper check rather than an independent vote — which is what the\n// conformance contract's header says.\n//","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue.go#L303-L339","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the wrapped cause (%w) for the underlying SQL/driver error and fix that first.","Retry the update once the database is reachable; the transaction rolled back so state is consistent.","Verify the issues/wisps and dependency tables match expected migrations.","Check UseWispsTable routing — ensure the correct table variant exists for the store."],"exampleFix":"// before\nerr := store.Update(ctx, id, opts) // opaque failure mid-status-change\n// after\nif err := store.Update(ctx, id, opts); err != nil {\n    if errors.Is(err, context.DeadlineExceeded) {\n        err = store.Update(ctx, id, opts) // safe: transactional, rolled back\n    }\n}","handlingStrategy":"retry","validationCode":"if _, err := store.Get(ctx, id, nil); err != nil { return err }\nif err := ctx.Err(); err != nil { return err }","typeGuard":null,"tryCatchPattern":"if err := store.Update(ctx, id, opts); err != nil {\n    var transient = errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF)\n    if transient { err = store.Update(ctx, id, opts) }\n}","preventionTips":["Keep update contexts generously timed for status changes with many dependents.","Keep schema migrations current before running updates.","Avoid manual edits to dependency tables.","Use one canonical code path for status updates so wisp routing stays correct."],"tags":["database","transaction","status-change"],"backgroundTag":"db-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}