{"record":{"id":"81a9fb72a75ed7eb","repo":"gastownhall/beads","slug":"w-issue-s-81a9fb","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"validation","errorClass":"storage.ErrNotFound","httpStatus":404,"severity":"warning","filePath":"internal/storage/issueops/delete.go","lineNumber":68,"sourceCode":"\treturn nil\n}\n\n//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)\nfunc deleteIssueRowInTx(ctx context.Context, tx *sql.Tx, id string, isWisp bool) error {\n\tissueTable, _, _, _ := WispTableRouting(isWisp)\n\tresult, err := tx.ExecContext(ctx, fmt.Sprintf(\"DELETE FROM %s WHERE id = ?\", issueTable), id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"delete issue from %s: %w\", issueTable, err)\n\t}\n\trows, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get rows affected: %w\", err)\n\t}\n\tif rows == 0 {\n\t\t// Wrap the sentinel so callers can errors.Is(..., storage.ErrNotFound),\n\t\t// matching GetIssue/UpdateIssue. The storage conformance suite asserts\n\t\t// this parity across not-found paths.\n\t\treturn fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t}\n\t// Journal the delete in the same transaction. This worker backs single\n\t// deletes (DeleteIssueInTx) and the per-wisp branch of the bulk delete\n\t// (DeleteResolvedSetInTx); the bulk regular-issue branch journals its own\n\t// ids directly. The rows==0 return above is what keeps this\n\t// actually-deleted-only. The delete plumbing (storage.DeleteIssue and the\n\t// bulk/cascade resolvers) carries no actor, so the row records none.\n\tif err := RecordDeleteInTx(ctx, tx, id, \"\"); err != nil {\n\t\treturn err\n\t}\n\tif isWisp {\n\t\tif err := DeleteWispFromDependenciesInTx(ctx, tx, id); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if err := DeleteLeaseInTx(ctx, tx, id); err != nil {\n\t\t// A deleted issue holds no lease.\n\t\treturn err\n\t}","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/delete.go#L50-L86","documentation":"Not a crash: this is the deliberate not-found return. When the DELETE affected zero rows, deleteIssueRowInTx returns storage.ErrNotFound wrapped with the issue id, so callers can use errors.Is(err, storage.ErrNotFound) — the same contract as GetIssue/UpdateIssue. It means the id did not exist (already deleted, wrong id, or it was in the other table than routed to).","triggerScenarios":"Deleting an id that was already deleted (double-delete or a race between two concurrent deletes); passing a typo'd or stale id; an id that lives in the wisp table being routed as a regular issue or vice versa.","commonSituations":"UI retry after a first delete already succeeded; scripts operating on an exported/cached issue list that is out of date; deleting an id that was never created in this database.","solutions":["Handle it as an expected outcome: check errors.Is(err, storage.ErrNotFound) and treat as success/idempotent no-op if appropriate.","Verify the id is correct (bd show <id> before deleting).","If the id should exist, check for a recent concurrent delete that removed it.","Re-run 'bd doctor' if you suspect wisp-vs-regular routing confusion for a fresh issue."],"exampleFix":"// before\nif err := storage.DeleteIssue(ctx, db, id); err != nil { return err }\n// after: treat not-found as idempotent success\nif err := storage.DeleteIssue(ctx, db, id); err != nil && !errors.Is(err, storage.ErrNotFound) {\n\treturn err\n}","handlingStrategy":"type-guard","validationCode":"// before deleting, confirm the id exists\nif _, err := storage.GetIssue(ctx, db, id); err != nil {\n\tif errors.Is(err, storage.ErrNotFound) { return nil } // nothing to delete\n\treturn err\n}","typeGuard":"func isIssueNotFound(err error) bool {\n\treturn errors.Is(err, storage.ErrNotFound)\n}","tryCatchPattern":"if err := storage.DeleteIssue(ctx, db, id); err != nil {\n\tif isIssueNotFound(err) {\n\t\treturn nil // idempotent: already deleted\n\t}\n\treturn err\n}","preventionTips":["Always match with errors.Is(err, storage.ErrNotFound), never string comparison.","Treat not-found on delete as success for idempotent scripts/retries.","Verify ids with bd show before bulk deletes built from cached lists."],"tags":["not-found","delete","idempotency","sentinel-error"],"backgroundTag":"issue-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}