gastownhall/beads · error

db: Update %s: %w

Error message

db: Update %s: %w

What it means

Wraps the case where Update reads the prior issue row and gets sql.ErrNoRows — the target issue does not exist. Beads deliberately re-wraps ErrNoRows with the 'db: Update %s: %w' prefix so callers can still detect absence via errors.Is(err, sql.ErrNoRows) while retaining the ID context.

Source

Thrown at internal/storage/domain/db/issue.go:172

			if val, ok := raw.(string); ok {
				if err := types.CheckFieldLen(field, val); err != nil {
					return err
				}
			}
		}
	}

	table := pickIssueTable(opts.UseWispsTable)

	mergeOps := issueops.HasMergeOps(updates)

	// Read the prior row once. Status and merge updates need it for their
	// transaction-local resolution, and every update uses it to suppress true
	// no-ops before changing row_lock or recording an event.
	oldIssue, err := r.Get(ctx, id, opts)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return fmt.Errorf("db: Update %s: %w", id, sql.ErrNoRows)
		}
		return fmt.Errorf("db: Update %s: read old issue: %w", id, err)
	}

	// Resolve read-merge-write operation keys (issueops.OpMergeMetadata,
	// OpSetMetadata, OpUnsetMetadata, OpAppendNotes) into concrete column
	// values inside the mutation transaction, mirroring the embedded path
	// (issueops.updateIssueInTx). Callers must pass the OPERATION, never a
	// value pre-merged from an earlier read: this runner is a Dolt sql-server
	// session where FOR UPDATE is a parse-only no-op, so a stale-snapshot merge
	// is only made safe by Dolt's commit-time conflict detection plus the
	// caller redoing the whole unit of work on a serialization failure — and
	// that redo re-runs this in-transaction resolution against the winner's
	// committed row.
	if mergeOps {
		resolved, err := issueops.ResolveMergeOps(oldIssue, updates)
		if err != nil {
			return fmt.Errorf("db: Update %s: %w", id, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Confirm the issue exists with bd show <id> or a Get call before updating
  2. Handle errors.Is(err, sql.ErrNoRows) explicitly in the caller as 'not found', not as an infrastructure fault
  3. Check whether the ID belongs to the wisps vs persistent table and pass matching IssueTableOpts
  4. Refresh any cached ID lists after concurrent operations

Example fix

// before
if err := repo.Update(ctx, id, updates, actor, opts); err != nil { return err }
// after
if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
    if errors.Is(err, sql.ErrNoRows) { return ErrIssueNotFound }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

if id == "" { return fmt.Errorf("issue ID required") }
if _, err := repo.Get(ctx, id, opts); err != nil {
    if errors.Is(err, sql.ErrNoRows) { return ErrIssueNotFound }
    return err
}

Type guard

func isIssueNotFound(err error) bool { return errors.Is(err, sql.ErrNoRows) }

Try / catch

if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
    if isIssueNotFound(err) {
        log.Warnf("issue %s vanished; skipping update", id)
        return ErrIssueNotFound
    }
    return err
}

Prevention

When it happens

Trigger: Calling Update with an issue ID not present in the table: already-deleted issue, typo in ID, wrong table (ephemeral vs persistent mismatch), or a race where another process deleted the row between fetch and update.

Common situations: Stale ID from a cached list after concurrent deletion; passing a wisp ID where the persistent table is queried; user-supplied ID with a typo.

Related errors


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