gastownhall/beads · error
db: Update %s: read old issue: %w
Error message
db: Update %s: read old issue: %w
What it means
Wraps any non-ErrNoRows failure from r.Get when Update reads the prior row prior to applying changes. This distinguishes real read failures (connection, schema, context) from the missing-row case handled separately. The update aborts before any mutation.
Source
Thrown at internal/storage/domain/db/issue.go:174
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)
}
updates = resolvedView on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause from r.Get (driver error, context error, scan error)
- Run migrations if schema/table issues are indicated
- Increase the context timeout or retry on transient errors
- Check row integrity if scan errors appear (NULLs in NOT-NULL-assumed columns)
Example fix
// before
err := repo.Update(ctx, id, updates, actor, opts)
// after
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
if !errors.Is(err, sql.ErrNoRows) { log.Warnf("read failure: %v", err) }
} Defensive patterns
Strategy: retry
Validate before calling
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := ctx.Err(); err != nil { return err } Type guard
func isReadFailure(err error) bool {
return err != nil && !errors.Is(err, sql.ErrNoRows)
} Try / catch
err := retry(3, backoff, func() error {
err := repo.Update(ctx, id, updates, actor, opts)
if err != nil && errors.Is(err, sql.ErrNoRows) { return stopRetry(err) }
if err != nil && !isTransientDBErr(err) { return stopRetry(err) }
return err
}) Prevention
- Keep context deadlines generous for read-modify-write updates
- Ensure schema consistency after migrations
- Monitor DB connectivity and restarts
- Check row data integrity if scan errors recur
When it happens
Trigger: Calling Update when the internal Get fails: connection loss, context canceled/cancelled deadline, corrupted row failing scan, missing table on the non-wisp path, or lock contention blocking the read inside a transaction.
Common situations: Database restarted mid-operation; context timeout too short for the read; schema drift after a failed migration; row with unexpected NULL columns breaking scan.
Related errors
- db: Update %s: %w
- db: MovePersistence %s: get issue: %w
- db: MovePersistence %s: %w
- rewrite refs %s: %w
- ErrTransaction
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/28cffe6ed7b3866c.
Report an issue: GitHub.