gastownhall/beads · error · storage.ErrNotFound
%w: issue %s
Error message
%w: issue %s
What it means
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.
Source
Thrown at internal/storage/issueops/metadata.go:72
}
// readMetadataMapInTx reads an issue's metadata column (routed to issues/wisps)
// and unmarshals it into a raw-value map. Existing values are kept as raw JSON so
// they round-trip byte-for-byte. An empty or null metadata column yields a fresh
// map; a missing issue returns a wrapped storage.ErrNotFound (mirroring
// CloseIssueInTx).
//
//nolint:gosec // G201: table name comes from WispTableRouting (hardcoded constants)
func readMetadataMapInTx(ctx context.Context, tx DBTX, issueID string) (map[string]json.RawMessage, error) {
isWisp := IsActiveWispInTx(ctx, tx, issueID)
issueTable, _, _, _ := WispTableRouting(isWisp)
var raw sql.NullString
err := tx.QueryRowContext(ctx,
fmt.Sprintf("SELECT metadata FROM %s WHERE id = ?", issueTable), issueID,
).Scan(&raw)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, issueID)
}
if err != nil {
return nil, fmt.Errorf("read metadata for %s: %w", issueID, err)
}
m := make(map[string]json.RawMessage)
if raw.Valid && raw.String != "" && raw.String != "null" {
if err := json.Unmarshal([]byte(raw.String), &m); err != nil {
return nil, fmt.Errorf("parse metadata for %s: %w", issueID, err)
}
}
return m, nil
}
// writeMergedMetadataInTx validates the fully-merged metadata blob against the
// configured schema (preserving the check the generic update path runs in the
// store wrapper) and then writes it via UpdateIssueInTx, which records the
// EventUpdated history event, normalizes the value, and bumps updated_at — allView on GitHub (pinned to 71377f2769)
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
Example fix
// before
if err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {
return err
}
// after
if err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {
if errors.Is(err, storage.ErrNotFound) {
return fmt.Errorf("issue %s does not exist; cannot merge metadata", id)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
var exists int
err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM issues WHERE id = ?", issueID).Scan(&exists)
if err != nil { return err }
if exists == 0 { return fmt.Errorf("issue %s not found", issueID) } Type guard
func isNotFound(err error) bool { return errors.Is(err, storage.ErrNotFound) } Try / catch
if err := issueops.MergeMetadataInTx(ctx, tx, id, patch); err != nil {
if errors.Is(err, storage.ErrNotFound) {
// handle missing issue: skip, create, or report
return fmt.Errorf("issue %s not found", id)
}
return err
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- no store is open for this workspace
- wisp %s not found
- failed to get issue for update: %w
- merge slot not found: %s
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f7675a642b80cfd6.
Report an issue: GitHub.