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 = resolved

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause from r.Get (driver error, context error, scan error)
  2. Run migrations if schema/table issues are indicated
  3. Increase the context timeout or retry on transient errors
  4. 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

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


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