{"record":{"id":"92fb50672eb0cbf4","repo":"gastownhall/beads","slug":"update-wisp-id-w","errorCode":null,"errorMessage":"update wisp ID: %w","messagePattern":"update wisp ID: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/bulk_ops.go","lineNumber":334,"sourceCode":"\n\treturn InsertDerivedEvent(ctx, tx, \"events\", AuxEvent{\n\t\tIssueID:   newID,\n\t\tEventType: \"renamed\",\n\t\tActor:     actor,\n\t\tOldValue:  str(oldID),\n\t\tNewValue:  str(newID),\n\t})\n}\n\nfunc updateWispIDInTx(ctx context.Context, tx *sql.Tx, oldID, newID string, issue *types.Issue, actor string) error {\n\tnow := time.Now().UTC()\n\tresult, err := tx.ExecContext(ctx, `\n\t\tUPDATE wisps\n\t\tSET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?\n\t\tWHERE id = ?\n\t`, newID, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, now, oldID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"update wisp ID: %w\", err)\n\t}\n\tif rows, _ := result.RowsAffected(); rows == 0 {\n\t\treturn fmt.Errorf(\"wisp not found: %s\", oldID)\n\t}\n\n\tif err = InsertDerivedEvent(ctx, tx, \"wisp_events\", AuxEvent{\n\t\tIssueID:   newID,\n\t\tEventType: \"renamed\",\n\t\tActor:     actor,\n\t\tOldValue:  str(oldID),\n\t\tNewValue:  str(newID),\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\treturn UpdateWispIDInDependenciesInTx(ctx, tx, oldID, newID)\n}\n","sourceCodeStart":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/bulk_ops.go#L316-L352","documentation":"This error wraps a failure of the SQL UPDATE that renames a wisp row (`UPDATE wisps SET id = ? ...`). Wisps are ephemeral derived issues stored in a separate table; when the UPDATE fails at the driver level the rename transaction aborts and the wisp keeps its old ID.","triggerScenarios":"Calling UpdateIssueIDInTx on an issue that IsActiveWispInTx reports as a wisp, and `UPDATE wisps` fails — unique-key collision on newID in wisps, invalid newID, or a driver/connection error.","commonSituations":"Renaming to an ID that already exists as another wisp; wisp table schema drift after upgrade; connection loss mid-transaction; malformed target ID rejected by the storage engine.","solutions":["Ensure newID does not collide with an existing wisp or issue before renaming.","Validate the new ID format (non-empty, allowed characters).","Inspect the wrapped driver error for the exact constraint and fix the input.","Bring schema/migrations up to date and retry in a fresh transaction."],"exampleFix":"// before\nstore.UpdateIssueID(ctx, oldID, dupID, issue, actor) // dupID exists in wisps -> constraint violation\n// after\nif err := ensureIDFree(ctx, store, dupID); err != nil {\n    return fmt.Errorf(\"choose a different ID: %w\", err)\n}\nstore.UpdateIssueID(ctx, oldID, dupID, issue, actor)","handlingStrategy":"validation","validationCode":"func validateWispRename(ctx context.Context, store Store, newID string) error {\n    if newID == \"\" { return errors.New(\"new id must not be empty\") }\n    if existing, _ := store.GetIssue(ctx, newID); existing != nil { return fmt.Errorf(\"id %s already in use\", newID) }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"if err := store.UpdateIssueID(ctx, oldID, newID, issue, actor); err != nil {\n    if strings.Contains(err.Error(), \"update wisp ID:\") {\n        return fmt.Errorf(\"wisp rename rejected (check ID collision/format): %w\", err)\n    }\n    return err\n}","preventionTips":["Validate target ID format and uniqueness for both issues and wisps.","Keep wisp/aux schema migrations in sync with the binary version.","Retry aborted renames; the transaction guarantees no partial rename."],"tags":["database","sql","wisp","rename"],"backgroundTag":"sql-constraint-violation","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}