{"record":{"id":"735c4e11b017105d","repo":"gastownhall/beads","slug":"w-s-735c4e","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"validation","errorClass":"storage.ErrAlreadyExists","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/create_only_guard.go","lineNumber":30,"sourceCode":"// EnsureIssueIDAvailableInTx serializes same-shard creates and rejects occupied IDs.\nfunc EnsureIssueIDAvailableInTx(ctx context.Context, tx DBTX, id string) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"ensure issue ID available: transaction is nil\")\n\t}\n\tif id == \"\" {\n\t\treturn fmt.Errorf(\"ensure issue ID available: ID is empty\")\n\t}\n\tkey := issueCreateCoordinationKey(id)\n\tif _, err := tx.ExecContext(ctx, \"REPLACE INTO local_metadata (`key`, value) VALUES (?, ?)\", key, strconv.FormatInt(FreshRowLock(), 10)); err != nil {\n\t\treturn fmt.Errorf(\"coordinate issue create: %w\", err)\n\t}\n\tfor _, table := range []string{\"issues\", \"wisps\"} {\n\t\tvar count int\n\t\tif err := tx.QueryRowContext(ctx, \"SELECT COUNT(*) FROM \"+table+\" WHERE id = ?\", id).Scan(&count); err != nil {\n\t\t\treturn fmt.Errorf(\"check %s for issue %q: %w\", table, id, err)\n\t\t}\n\t\tif count > 0 {\n\t\t\treturn fmt.Errorf(\"%w: %s\", storage.ErrAlreadyExists, id)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc issueCreateCoordinationKey(id string) string {\n\tsum := sha256.Sum256([]byte(id))\n\tshard := uint16(sum[0])<<4 | uint16(sum[1])>>4\n\treturn fmt.Sprintf(\"issue-create/v1/%03x\", shard)\n}\n","sourceCodeStart":12,"sourceCodeEnd":41,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/create_only_guard.go#L12-L41","documentation":"The sentinel duplicate error: the ID was found in the issues or wisps table, so EnsureIssueIDAvailableInTx returns fmt.Errorf(\"%w: %s\", storage.ErrAlreadyExists, id). Callers match it with errors.Is(err, storage.ErrAlreadyExists). It is the guard's normal refusal for occupied IDs, raised after coordination serializes the create.","triggerScenarios":"CreateIssueInTxWithResult called with an ID that already exists in either issues or wisps — including a wisp holding the same ID as a would-be regular issue (the guard checks both tables).","commonSituations":"Re-importing an exported file without skip-existing logic; ID collisions between regular issues and wisps; concurrent workers generating the same ID.","solutions":["Match with errors.Is(err, storage.ErrAlreadyExists) and switch to update-or-skip logic for that ID","Generate a fresh unique ID for the new issue and retry","Check existence up front (or prefix IDs with a per-run unique token) before batch import"],"exampleFix":"// before\nerr := CreateIssueInTxWithResult(ctx, tx, issue) // panics on dup at call site\n// after\nif err := CreateIssueInTxWithResult(ctx, tx, issue); errors.Is(err, storage.ErrAlreadyExists) {\n    return skipOrCreateFreshID(issue)\n}","handlingStrategy":"type-guard","validationCode":"var count int\nrow := db.QueryRow(\"SELECT (SELECT COUNT(*) FROM issues WHERE id=?) + (SELECT COUNT(*) FROM wisps WHERE id=?)\", id, id)\nrow.Scan(&count)\n// count > 0 means the create will be refused","typeGuard":"func isAlreadyExists(err error) bool { return errors.Is(err, storage.ErrAlreadyExists) }","tryCatchPattern":"if err := CreateIssueInTxWithResult(ctx, tx, issue); err != nil {\n    if isAlreadyExists(err) { /* skip, update, or mint a new ID */ }\n    return err\n}","preventionTips":["Check both issues and wisps for ID collisions before import","Use unique ID generation (prefix + timestamp/counter)","Decide up-front whether imports skip or update existing IDs"],"tags":["duplicate","create","already-exists","id-collision"],"backgroundTag":"issue-already-exists","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}