gastownhall/beads · error
their values for %s %s matched no row (was it deleted concur
Error message
their values for %s %s matched no row (was it deleted concurrently?); conflict left unresolved
What it means
This error comes from resolveOneConflictRow when resolving a modify/modify dolt conflict with the 'theirs' strategy: the UPDATE writing the peer's values matched zero rows, and a follow-up existence check (conflictTargetStillPresent) confirmed the target row is genuinely gone from the working table. Since their values cannot be applied to a nonexistent row, the conflict row is deliberately left in dolt_conflicts_<table> and the resolution aborts rather than silently discarding one side.
Source
Thrown at internal/storage/versioncontrolops/conflicts.go:424
stmt := fmt.Sprintf("UPDATE `%s` SET %s WHERE `%s` = ?", table, strings.Join(sets, ", "), keyCol) //nolint:gosec // identifiers validated above
res, err := db.ExecContext(ctx, stmt, args...)
if err != nil {
return fmt.Errorf("apply their values for %s %s: %w", table, key, err)
}
// Zero rows would mean the row we read the conflict for is no longer
// there — another session on the same branch deleted it between the
// read and the write. Clearing the conflict now would discard their
// side under a --theirs invocation, undetectably. But zero is not
// proof of that on its own (see conflictTargetStillPresent), so ask
// before refusing: an operator who named this row deserves the abort
// only when the row really is gone.
if n, err := res.RowsAffected(); err != nil || n == 0 {
present, err := conflictTargetStillPresent(ctx, db, table, keyCol, ourKey)
if err != nil {
return fmt.Errorf("confirm %s %s still exists after writing their values: %w", table, key, err)
}
if !present {
return fmt.Errorf("their values for %s %s matched no row (was it deleted concurrently?); conflict left unresolved", table, key)
}
}
}
del := fmt.Sprintf("DELETE FROM `dolt_conflicts_%s` WHERE `our_%s` = ?", table, keyCol) //nolint:gosec // identifiers validated
res, err := db.ExecContext(ctx, del, ourKey)
if err != nil {
return fmt.Errorf("clear conflict for %s %s: %w", table, key, err)
}
if n, err := res.RowsAffected(); err == nil && n == 0 {
return fmt.Errorf("conflict for %s %s was not cleared (no conflict row deleted)", table, key)
}
return nil
}
// GetMergeBlockers reports the merge state that `bd conflicts` cannot show as
// rows: whether a merge is open at all, plus the schema conflicts and
// constraint violations that make dolt refuse the merge commit even whenView on GitHub (pinned to 71377f2769)
Solutions
- Re-list the conflicts and retry only rows still present in both dolt_conflicts_<table> and the base table.
- Re-run the merge/pull so the conflict set reflects the current working set before applying a whole-table strategy.
- Use the 'ours' strategy for rows that legitimately no longer exist, or delete the conflict row explicitly if you intend to accept the deletion.
- Serialize resolution: ensure only one process resolves conflicts on a branch at a time (lock or a single bd session).
Example fix
// before: resolve stale rows by keys captured earlier
err := ops.ResolveConflictRows(ctx, db, table, "theirs", keys)
// after: re-verify each row exists before resolving
for _, k := range keys {
var n int
_ = db.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM `%s` WHERE `%s` = ?", table, keyCol), k).Scan(&n)
if n == 0 { continue } // row gone; skip instead of aborting mid-batch
err = ops.ResolveConflictRows(ctx, db, table, "theirs", []string{k})
} Defensive patterns
Strategy: validation
Validate before calling
func rowExists(ctx context.Context, db *sql.DB, table, keyCol string, key any) (bool, error) {
var n int
err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM `%s` WHERE `%s` = ?", table, keyCol), key).Scan(&n)
return n > 0, err
}
// call before ResolveConflictRows and skip rows that no longer exist Try / catch
if err := ops.ResolveConflictRows(ctx, db, table, "theirs", keys); err != nil {
if strings.Contains(err.Error(), "matched no row") {
return refreshAndRetry(ctx, db, table) // row vanished concurrently
}
return err
} Prevention
- Resolve conflicts from a single session per branch; no concurrent resolutions or other writers.
- Re-list dolt_conflicts immediately before resolving instead of caching keys.
- Avoid deleting rows on the branch while a merge is open and being resolved.
When it happens
Trigger: Calling ResolveConflictRows with the 'theirs' strategy on a row whose key no longer exists in the working table — typically because another session or bd/dolt process deleted that row between the conflict-table read and the UPDATE. Also possible on the autocommit (embedded Pull) path where a delete+reinsert race window exists between reading the conflict and writing their values.
Common situations: Two operators resolving the same branch's merge conflicts at once; a cleanup job deleting rows concurrently with resolution; another process committing to the same working set; split-brain access to the same dolt database from server mode and embedded mode.
Related errors
- conflict for %s %s was not cleared (no conflict row deleted)
- ErrStoreClosed
- %s of %s reported success but did not land (found assignee=%
- failed to get conflicts: %w
- failed to scan conflict: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/fe16ae15178f090a.
Report an issue: GitHub.