{"record":{"id":"f7675a642b80cfd6","repo":"gastownhall/beads","slug":"w-issue-s-f7675a","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"exception","errorClass":"storage.ErrNotFound","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/metadata.go","lineNumber":72,"sourceCode":"}\n\n// readMetadataMapInTx reads an issue's metadata column (routed to issues/wisps)\n// and unmarshals it into a raw-value map. Existing values are kept as raw JSON so\n// they round-trip byte-for-byte. An empty or null metadata column yields a fresh\n// map; a missing issue returns a wrapped storage.ErrNotFound (mirroring\n// CloseIssueInTx).\n//\n//nolint:gosec // G201: table name comes from WispTableRouting (hardcoded constants)\nfunc readMetadataMapInTx(ctx context.Context, tx DBTX, issueID string) (map[string]json.RawMessage, error) {\n\tisWisp := IsActiveWispInTx(ctx, tx, issueID)\n\tissueTable, _, _, _ := WispTableRouting(isWisp)\n\n\tvar raw sql.NullString\n\terr := tx.QueryRowContext(ctx,\n\t\tfmt.Sprintf(\"SELECT metadata FROM %s WHERE id = ?\", issueTable), issueID,\n\t).Scan(&raw)\n\tif err == sql.ErrNoRows {\n\t\treturn nil, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, issueID)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read metadata for %s: %w\", issueID, err)\n\t}\n\n\tm := make(map[string]json.RawMessage)\n\tif raw.Valid && raw.String != \"\" && raw.String != \"null\" {\n\t\tif err := json.Unmarshal([]byte(raw.String), &m); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"parse metadata for %s: %w\", issueID, err)\n\t\t}\n\t}\n\treturn m, nil\n}\n\n// writeMergedMetadataInTx validates the fully-merged metadata blob against the\n// configured schema (preserving the check the generic update path runs in the\n// store wrapper) and then writes it via UpdateIssueInTx, which records the\n// EventUpdated history event, normalizes the value, and bumps updated_at — all","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/metadata.go#L54-L90","documentation":"readMetadataMapInTx wraps storage.ErrNotFound when the SELECT of the metadata column for the given issue ID returns sql.ErrNoRows. It signals that no issue with that ID exists in the issues table, so there is no metadata map to read, merge, delete, or compare-and-set. It is a sentinel-wrapped error, so callers can match with errors.Is against storage.ErrNotFound.","triggerScenarios":"Calling MergeMetadataInTx, DeleteMetadataInTx, or CompareAndSetMetadataKeyInTx with an issueID that does not exist in the issues table (typo'd ID, issue deleted in another transaction, or the ID exists only as a wisp/ephemeral row not visible in that plane).","commonSituations":"Scripts operating on IDs from stale exports or other clones; passing a wisp ID to a metadata op that reads the durable issues table; a race where the issue was deleted between listing and updating; missing bd sync so the local DB lacks the issue.","solutions":["Verify the issue ID exists first with bd show <id> or GetIssue before calling the metadata mutation","Check errors.Is(err, storage.ErrNotFound) and surface a clear 'issue not found' message to the user instead of a raw DB error","If the ID may be a wisp/ephemeral issue, use the wisp-plane accessor or promote it first","Sync the database (bd dolt pull / bd sync) if the issue should exist but was created elsewhere"],"exampleFix":"// before\nif err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {\n\treturn err\n}\n// after\nif err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {\n\tif errors.Is(err, storage.ErrNotFound) {\n\t\treturn fmt.Errorf(\"issue %s does not exist; cannot merge metadata\", id)\n\t}\n\treturn err\n}","handlingStrategy":"try-catch","validationCode":"var exists int\nerr := tx.QueryRowContext(ctx, \"SELECT COUNT(*) FROM issues WHERE id = ?\", issueID).Scan(&exists)\nif err != nil { return err }\nif exists == 0 { return fmt.Errorf(\"issue %s not found\", issueID) }","typeGuard":"func isNotFound(err error) bool { return errors.Is(err, storage.ErrNotFound) }","tryCatchPattern":"if err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {\n\tif errors.Is(err, storage.ErrNotFound) {\n\t\t// handle missing issue: skip, create, or report\n\t\treturn fmt.Errorf(\"issue %s not found\", id)\n\t}\n\treturn err\n}","preventionTips":["Always match with errors.Is(err, storage.ErrNotFound) rather than string comparison","Resolve issue IDs from API responses, never free-typed input","Sync your clone before operating on IDs created elsewhere","Remember wisps live on a separate plane; check the right table for ephemeral issues"],"tags":["go","storage","not-found","metadata"],"backgroundTag":"issue-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}