gastownhall/beads · error

merged values for issue %v matched no row (was it deleted co

Error message

merged values for issue %v matched no row (was it deleted concurrently?); conflict left unresolved

What it means

The merged-values UPDATE matched zero rows and the follow-up existence check confirmed the issue row is gone: another session deleted the issue between the conflict scan and the write. Clearing the conflict in that state would silently discard the deleted side's change, so the code refuses and leaves the conflict unresolved with this error.

Source

Thrown at internal/storage/versioncontrolops/automerge.go:617

			stmt := fmt.Sprintf("UPDATE `issues` SET %s WHERE `%s` = ?", strings.Join(sets, ", "), issuesKeyColumn) //nolint:gosec // identifiers validated above
			res, err := db.ExecContext(ctx, stmt, args...)
			if err != nil {
				return fmt.Errorf("apply merged values for issue %v: %w", m.ourKey, err)
			}
			// Zero rows would mean the row we planned against is gone —
			// another session deleted it between the read and the write, and
			// clearing the conflict now would discard their side undetectably.
			// But RowsAffected is rows CHANGED, not rows MATCHED: the DSN does
			// not set clientFoundRows (doltutil/dsn.go), so a write the backend
			// normalizes to the bytes already stored also reports zero. Only a
			// follow-up existence check can tell "vanished" from "no-op".
			if n, err := res.RowsAffected(); err != nil || n == 0 {
				present, err := conflictTargetStillPresent(ctx, db, "issues", issuesKeyColumn, m.ourKey)
				if err != nil {
					return fmt.Errorf("confirm issue %v still exists after writing merged values: %w", m.ourKey, err)
				}
				if !present {
					return fmt.Errorf("merged values for issue %v matched no row (was it deleted concurrently?); conflict left unresolved", m.ourKey)
				}
			}
		}
		res, err := db.ExecContext(ctx,
			"DELETE FROM dolt_conflicts_issues WHERE our_"+issuesKeyColumn+" = ?", m.ourKey)
		if err != nil {
			return fmt.Errorf("clear conflict for issue %v: %w", m.ourKey, err)
		}
		if n, err := res.RowsAffected(); err == nil && n == 0 {
			return fmt.Errorf("conflict for issue %v was not cleared (no conflict row deleted)", m.ourKey)
		}
	}
	return nil
}

// unionConflictsAreSafe reports whether every live conflict of a union-merged
// table (labels, comments, events) is the same row on both sides with matching
// columns — the only class where "union" has an unambiguous answer. A row

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-run `bd dolt pull` / merge: the deletion will now conflict or merge normally, and the stale plan is discarded.
  2. Check whether the deletion was intended; if not, restore the issue from the other branch/parent (`bd dolt` parent queries or git-style history) and retry.
  3. Serialize concurrent sessions — ensure only one `bd` process merges at a time (e.g. take a lock or re-run after the other finishes).
  4. If the deletion is correct, resolve the conflict manually toward the deleted side (delete our row / DOLT_CONFLICTS_RESOLVE) instead of relying on auto-merge.

Example fix

// before: auto-merge proceeds even though a concurrent delete may have landed
err := TryAutoResolveMergeConflicts(ctx, db)
// after: detect concurrent-delete errors and fall back to a re-sync
if err := TryAutoResolveMergeConflicts(ctx, db); err != nil {
    if strings.Contains(err.Error(), "deleted concurrently") {
        // re-pull to pick up the deletion, then re-attempt
        return pullAndRetry(ctx, db)
    }
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

// verify the issue still exists immediately before merging
var exists int
err := db.QueryRowContext(ctx,
    "SELECT COUNT(*) FROM `issues` WHERE `id` = ?", issueID).Scan(&exists)
if err != nil || exists == 0 {
    return fmt.Errorf("issue %v vanished before merge; re-run pull", issueID)
}

Type guard

func isConcurrentDelete(err error) bool {
    return err != nil && strings.Contains(err.Error(), "deleted concurrently")
}

Try / catch

if err := TryAutoResolveMergeConflicts(ctx, db); err != nil {
    if isConcurrentDelete(err) {
        // fall back to a fresh pull that will merge/pick up the deletion
        return pullAndRetryMerge(ctx, db)
    }
    return err
}

Prevention

When it happens

Trigger: Between issuesConflictsAreFieldMergeable reading dolt_conflicts_issues and the UPDATE in resolveIssuesFieldMerge, a concurrent session (or a rebase/purge command) deleted the issue identified by m.ourKey from the working set, so `UPDATE issues ... WHERE <key> = ?` matched nothing and conflictTargetStillPresent returns false.

Common situations: Two operators or an automation job merge/pull at the same time; one closed or deleted the issue while the other merged field changes for it; a cleanup script (bd gc / purge of closed issues) ran during a pull; CI merging a branch whose issues were deleted on the base branch.

Related errors


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