gastownhall/beads · error

db: Update %s: compare updates: %w

Error message

db: Update %s: compare updates: %w

What it means

Wrapped failure from Update's no-op comparison step: issueops.DiscardNoopIssueUpdates compares each update against the row already read and errors out instead of silently comparing when something is off (e.g. an incomparable value type or malformed update payload). Message is prefixed 'compare updates' to distinguish it from the other Update wraps.

Source

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

			return fmt.Errorf("db: Update %s: %w", id, err)
		}
		updates = resolved
	}

	// closed_at coherence parity with issueops.updateIssueInTx: an explicit
	// closed_at must agree with the status this update lands, checked against
	// the row this unit of work already read and ahead of the close-policy gate
	// so a refusal writes nothing at all. It runs on the merge-resolved map
	// BEFORE the no-op filter for the same reason it does there — the guard
	// reads the caller's intent, and a closed_at equal to the stored value is
	// still a request to keep the column, not an absent key.
	if err := issueops.ValidateClosedAtCoherence(oldIssue, updates); err != nil {
		return fmt.Errorf("db: Update %s: %w", id, err)
	}

	filteredUpdates, err := issueops.DiscardNoopIssueUpdates(oldIssue, updates)
	if err != nil {
		return fmt.Errorf("db: Update %s: compare updates: %w", id, err)
	}
	updates = filteredUpdates
	if len(updates) == 0 {
		return nil
	}
	// A status that matched the row was already dropped as a no-op, so the
	// lifecycle side effects below only fire on a real transition.
	_, statusChanging := updates["status"]

	// Close-policy parity with issueops.updateIssueInTx: a status that crosses
	// into the done category is a close by another name and answers to close
	// policy. A refusal returns before any write and aborts the caller's unit of
	// work. The wrap keeps the sentinels matchable, so a caller distinguishes
	// these refusals here exactly as it does on the close path.
	if statusChanging {
		crossing, err := issueops.CrossesIntoDoneCategoryInTx(ctx, r.runner, oldIssue.Status, updates)
		if err != nil {
			return fmt.Errorf("db: Update %s: %w", id, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the inner error to see which field failed comparison.
  2. Coerce update values to the column's Go type before calling Update (strings for text fields, typed values for timestamps/priority).
  3. If building maps from JSON, decode into typed structs first.
  4. Remove the offending key if the update is genuinely not needed.

Example fix

// before: JSON-decoded any value
updates := map[string]any{"priority": json.Number("1")}
// after: typed value
p, _ := json.Number("1").Int64()
updates := map[string]any{"priority": int(p)}
Defensive patterns

Strategy: validation

Validate before calling

// coerce values to column Go types before Update
updates["priority"] = int(p)         // not json.Number
updates["title"] = s                 // plain string
updates["estimated_minutes"] = int(m)

Type guard

func isComparableScalar(v any) bool {
    switch v.(type) {
    case string, int, int64, float64, bool, time.Time, nil:
        return true
    }
    return false
}

Try / catch

if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
    if strings.Contains(err.Error(), "compare updates") {
        // find offending field, fix its Go type, retry once
    }
}

Prevention

When it happens

Trigger: Calling Update with an updates map whose value types cannot be compared against the stored row (non-string values where strings are expected, unexpected shapes for typed columns), causing DiscardNoopIssueUpdates to return an error rather than a filtered map.

Common situations: Dynamic update maps built from JSON/CLI flags where values arrive as any/interface{} of the wrong type; version skew where a caller sends a field with a shape the comparator does not understand.

Related errors


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