gastownhall/beads · error

db: Update %s: rows affected: %w

Error message

db: Update %s: rows affected: %w

What it means

Wrapped failure from res.RowsAffected() after the UPDATE executed: the driver could not report how many rows the statement changed. This is a driver/protocol-level problem, distinct from a zero-row result (which yields sql.ErrNoRows instead). The unit of work aborts even though the UPDATE may have been applied.

Source

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

	// status/ownership mutation collides on this shared cell instead of
	// silently cell-merging, and the row's RowVersion CAS token advances so
	// the "generic update path changes RowVersion" contract
	// (types.Issue.RowVersion) holds on the proxied backend too.
	rowLockClause, rowLockArgs := issueops.RowLockClause()
	setClauses = append(setClauses, rowLockClause)
	args = append(args, rowLockArgs...)

	args = append(args, id)

	//nolint:gosec // G201: table is one of two hardcoded constants
	q := fmt.Sprintf("UPDATE %s SET %s WHERE id = ?", table, strings.Join(setClauses, ", "))
	res, err := r.runner.ExecContext(ctx, q, args...)
	if err != nil {
		return fmt.Errorf("db: Update %s: %w", id, err)
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return fmt.Errorf("db: Update %s: rows affected: %w", id, err)
	}
	if rows == 0 {
		return fmt.Errorf("db: Update %s: %w", id, sql.ErrNoRows)
	}
	if clearLease && !opts.UseWispsTable {
		if err := issueops.DeleteLeaseInTx(ctx, r.runner, id); err != nil {
			return fmt.Errorf("db: Update %s: clear lease: %w", id, err)
		}
	}

	// Event-type parity: embedded records EventClosed / EventReopened /
	// EventStatusChanged for status transitions (issueops.DetermineEventType),
	// EventUpdated otherwise.
	eventType := types.EventUpdated
	if statusChanging {
		eventType = issueops.DetermineEventType(oldIssue, updates)
	}
	if err := r.events.Record(ctx, domain.Event{

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped driver error and driver/server versions; upgrade the Dolt driver/server to a compatible pair.
  2. Retry the unit of work on a fresh connection; verify whether the update landed (read the row) before re-writing.
  3. Avoid proxy/middleware that interferes with result metadata.
  4. If reproducible, report the driver issue — this path should never error on a healthy server.

Example fix

// before
if err := repo.Update(ctx, id, updates, actor, opts); err != nil { return err }
// after: verify state before retrying
if err := repo.Update(ctx, id, updates, actor, opts); err != nil && isRowsAffectedErr(err) {
    if iss, _ := repo.Get(ctx, id, opts); iss == nil || !applied(iss, updates) { retry() }
}
Defensive patterns

Strategy: fallback

Validate before calling

// detect a driver that cannot report rows affected (best-effort probe)
_, err := conn.ExecContext(ctx, "SELECT 1")
if err != nil { /* degraded driver — avoid write paths or verify by read-back */ }

Try / catch

if err := repo.Update(ctx, id, updates, actor, opts); err != nil && strings.Contains(err.Error(), "rows affected") {
    // verify outcome by reading the row before retrying
    iss, gerr := repo.Get(ctx, id, opts)
    if gerr == nil && updateApplied(iss, updates) { return nil }
    return retry(freshCtx)
}

Prevention

When it happens

Trigger: Calling Update when the underlying driver (dolthub/driver via database/sql) fails to return rows-affected metadata — protocol mismatch, connection in a bad state after ExecContext, or a driver that does not support RowsAffected for this statement.

Common situations: Bugs or version skew in the Dolt driver; a connection dropped between ExecContext and RowsAffected; proxying layers that strip result metadata.

Related errors


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