{"record":{"id":"fe16ae15178f090a","repo":"gastownhall/beads","slug":"their-values-for-s-s-matched-no-row-was-it-dele","errorCode":null,"errorMessage":"their values for %s %s matched no row (was it deleted concurrently?); conflict left unresolved","messagePattern":"their values for (.+?) (.+?) matched no row \\(was it deleted concurrently\\?\\); conflict left unresolved","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/conflicts.go","lineNumber":424,"sourceCode":"\t\tstmt := fmt.Sprintf(\"UPDATE `%s` SET %s WHERE `%s` = ?\", table, strings.Join(sets, \", \"), keyCol) //nolint:gosec // identifiers validated above\n\t\tres, err := db.ExecContext(ctx, stmt, args...)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"apply their values for %s %s: %w\", table, key, err)\n\t\t}\n\t\t// Zero rows would mean the row we read the conflict for is no longer\n\t\t// there — another session on the same branch deleted it between the\n\t\t// read and the write. Clearing the conflict now would discard their\n\t\t// side under a --theirs invocation, undetectably. But zero is not\n\t\t// proof of that on its own (see conflictTargetStillPresent), so ask\n\t\t// before refusing: an operator who named this row deserves the abort\n\t\t// only when the row really is gone.\n\t\tif n, err := res.RowsAffected(); err != nil || n == 0 {\n\t\t\tpresent, err := conflictTargetStillPresent(ctx, db, table, keyCol, ourKey)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"confirm %s %s still exists after writing their values: %w\", table, key, err)\n\t\t\t}\n\t\t\tif !present {\n\t\t\t\treturn fmt.Errorf(\"their values for %s %s matched no row (was it deleted concurrently?); conflict left unresolved\", table, key)\n\t\t\t}\n\t\t}\n\t}\n\n\tdel := fmt.Sprintf(\"DELETE FROM `dolt_conflicts_%s` WHERE `our_%s` = ?\", table, keyCol) //nolint:gosec // identifiers validated\n\tres, err := db.ExecContext(ctx, del, ourKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"clear conflict for %s %s: %w\", table, key, err)\n\t}\n\tif n, err := res.RowsAffected(); err == nil && n == 0 {\n\t\treturn fmt.Errorf(\"conflict for %s %s was not cleared (no conflict row deleted)\", table, key)\n\t}\n\treturn nil\n}\n\n// GetMergeBlockers reports the merge state that `bd conflicts` cannot show as\n// rows: whether a merge is open at all, plus the schema conflicts and\n// constraint violations that make dolt refuse the merge commit even when","sourceCodeStart":406,"sourceCodeEnd":442,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/conflicts.go#L406-L442","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: resolve stale rows by keys captured earlier\nerr := ops.ResolveConflictRows(ctx, db, table, \"theirs\", keys)\n// after: re-verify each row exists before resolving\nfor _, k := range keys {\n  var n int\n  _ = db.QueryRowContext(ctx, fmt.Sprintf(\"SELECT COUNT(*) FROM `%s` WHERE `%s` = ?\", table, keyCol), k).Scan(&n)\n  if n == 0 { continue } // row gone; skip instead of aborting mid-batch\n  err = ops.ResolveConflictRows(ctx, db, table, \"theirs\", []string{k})\n}","handlingStrategy":"validation","validationCode":"func rowExists(ctx context.Context, db *sql.DB, table, keyCol string, key any) (bool, error) {\n  var n int\n  err := db.QueryRowContext(ctx, fmt.Sprintf(\"SELECT COUNT(*) FROM `%s` WHERE `%s` = ?\", table, keyCol), key).Scan(&n)\n  return n > 0, err\n}\n// call before ResolveConflictRows and skip rows that no longer exist","typeGuard":null,"tryCatchPattern":"if err := ops.ResolveConflictRows(ctx, db, table, \"theirs\", keys); err != nil {\n  if strings.Contains(err.Error(), \"matched no row\") {\n    return refreshAndRetry(ctx, db, table) // row vanished concurrently\n  }\n  return err\n}","preventionTips":["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."],"tags":["merge-conflicts","dolt","concurrency","theirs-strategy"],"backgroundTag":"conflict-resolution-race","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}