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 — all

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the issue ID exists first with bd show <id> or GetIssue before calling the metadata mutation
  2. Check errors.Is(err, storage.ErrNotFound) and surface a clear 'issue not found' message to the user instead of a raw DB error
  3. If the ID may be a wisp/ephemeral issue, use the wisp-plane accessor or promote it first
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/f7675a642b80cfd6. Report an issue: GitHub.