{"record":{"id":"799a8c6e0506e2a5","repo":"gastownhall/beads","slug":"ensure-issue-id-available-id-is-empty","errorCode":null,"errorMessage":"ensure issue ID available: ID is empty","messagePattern":"ensure issue ID available: ID is empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/create_only_guard.go","lineNumber":18,"sourceCode":"package issueops\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/steveyegge/beads/internal/storage\"\n)\n\n// 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 {","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/create_only_guard.go#L1-L36","documentation":"EnsureIssueIDAvailableInTx rejects an empty issue ID before serializing the create. An ID is mandatory: the guard uses it to write a coordination key and probe the issues/wisps tables, and an empty ID would collide across all unnamed creates. This is a defensive pre-condition error, not a data-store failure.","triggerScenarios":"Calling CreateIssueInTxWithResult (which calls this guard) with issue.ID == \"\" — e.g. an import path that assumed auto-ID generation but the transactional create path requires an explicit ID.","commonSituations":"Import code that forgot to assign IDs before the transactional create; refactored create flows that stopped defaulting IDs upstream.","solutions":["Assign a non-empty ID (e.g. prefix-<generated>) to the issue before the transactional create","Use the non-transactional create path that generates IDs automatically","Validate id != \"\" at the call site before opening the transaction"],"exampleFix":"// before\nerr := CreateIssueInTxWithResult(ctx, tx, issue) // issue.ID == \"\"\n// after\nif issue.ID == \"\" { issue.ID = GenerateID(prefix) }\nerr := CreateIssueInTxWithResult(ctx, tx, issue)","handlingStrategy":"validation","validationCode":"if issue.ID == \"\" {\n    return fmt.Errorf(\"issue %s needs an ID before transactional create\", issue.Title)\n}","typeGuard":"func hasID(i *types.Issue) bool { return i != nil && i.ID != \"\" }","tryCatchPattern":"if err := CreateIssueInTxWithResult(ctx, tx, issue); err != nil && strings.Contains(err.Error(), \"ID is empty\") {\n    // assign an ID and retry in a fresh transaction\n}","preventionTips":["Generate IDs before entering transactions","Centralize ID assignment in one helper","Never reuse the transactional create path for auto-ID flows"],"tags":["validation","create","empty-id","transaction"],"backgroundTag":"missing-issue-id","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}