{"record":{"id":"698ed3d60f2dd969","repo":"gastownhall/beads","slug":"conflict-for-s-s-is-not-a-modify-modify-conflict","errorCode":null,"errorMessage":"conflict for %s %s is not a modify/modify conflict (one side has no row); resolve it with a whole-table strategy or edit the row directly","messagePattern":"conflict for (.+?) (.+?) is not a modify/modify conflict \\(one side has no row\\); resolve it with a whole-table strategy or edit the row directly","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/conflicts.go","lineNumber":383,"sourceCode":"}\n\n// resolveOneConflictRow applies strategy to a single modify/modify row.\n//\n// \"ours\" is dolt's manual-resolution path: the working set already holds our\n// values, so deleting the conflict row *is* the resolution. \"theirs\" first\n// writes their values over ours, then deletes the conflict row — the order\n// matters, since the delete is what tells dolt the row is settled.\nfunc resolveOneConflictRow(ctx context.Context, db DBConn, table, keyCol, key, strategy string, row rawConflictRow) error {\n\tourKey, ourOK := row.value(\"our\", keyCol)\n\ttheirKey, theirOK := row.value(\"their\", keyCol)\n\tif !ourOK || !theirOK {\n\t\treturn fmt.Errorf(\"conflict table dolt_conflicts_%s has no our_%s/their_%s column\", table, keyCol, keyCol)\n\t}\n\tif ourKey == nil || theirKey == nil {\n\t\t// delete/modify (one side removed the row) or add/add against a\n\t\t// missing key: refuse by name. Row-level ours/theirs would silently\n\t\t// resurrect or destroy a row the operator never looked at.\n\t\treturn fmt.Errorf(\"conflict for %s %s is not a modify/modify conflict (one side has no row); \"+\n\t\t\t\"resolve it with a whole-table strategy or edit the row directly\", table, key)\n\t}\n\n\tif strategy == ConflictStrategyTheirs {\n\t\tnames, vals := row.theirFields(keyCol)\n\t\tif len(names) == 0 {\n\t\t\treturn fmt.Errorf(\"conflict for %s %s carries no their_* data columns\", table, key)\n\t\t}\n\t\tsets := make([]string, len(names))\n\t\targs := make([]any, 0, len(names)+1)\n\t\tfor i, n := range names {\n\t\t\t// Column names are interpolated (MySQL cannot bind an\n\t\t\t// identifier) and come from the conflict table's own schema,\n\t\t\t// which a peer's schema merge can extend — gate them exactly\n\t\t\t// like the table name rather than trusting the source.\n\t\t\tif err := ValidateConflictTable(n); err != nil {\n\t\t\t\treturn fmt.Errorf(\"refusing to write unexpected column %q of %s: %w\", n, table, err)\n\t\t\t}","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/conflicts.go#L365-L401","documentation":"The conflict is a delete/modify or add/add case: one branch has no row for this key (our_<keyCol> or their_<keyCol> is NULL). The library deliberately refuses row-level ours/theirs here because blindly applying 'theirs' would silently resurrect a deleted row or 'ours' would destroy one, without the operator ever seeing it. It must be resolved with a whole-table strategy or by hand-editing the row.","triggerScenarios":"ResolveConflictRows -> resolveOneConflictRow sees ourKey == nil or theirKey == nil for the named key — one side deleted the row while the other modified it, or both added rows that dolt records against a missing base key.","commonSituations":"Branch A deletes an issue while branch B edits it, then both merge; re-adding a row that was deleted on the other branch; bulk deletes colliding with concurrent edits during a sync.","solutions":["Re-create the row manually if you want it back, then clear the conflict","Delete the surviving row if the deletion is correct, then clear the conflict","Use a whole-table strategy (`--theirs`/`--ours` for the table) if you accept either side wholesale","Inspect both sides first: SELECT the our_/their_ columns of dolt_conflicts_<table> to see what each branch holds"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"// detect delete/modify conflicts up front\nrow, err := loadConflictRow(ctx, db, table, keyCol, key)\nif err != nil { return err }\nourKey, _ := row.value(\"our\", keyCol); theirKey, _ := row.value(\"their\", keyCol)\nif ourKey == nil || theirKey == nil {\n    return handleDeleteModify(ctx, db, table, keyCol, key) // explicit operator choice\n}","typeGuard":null,"tryCatchPattern":"err := resolveOne(ctx, db, table, keyCol, key, strategy)\nif err != nil && strings.Contains(err.Error(), \"not a modify/modify conflict\") {\n    // require explicit operator decision for delete/modify\n    return promptDeleteModifyResolution(ctx, db, table, keyCol, key)\n}\nreturn err","preventionTips":["Review delete/modify collisions before merging, not during resolution","Coordinate row deletions across branches (soft deletes instead of hard deletes)","Use whole-table strategy when many delete/modify conflicts are expected","Audit cross-branch row lifecycle in team workflows"],"tags":["database","dolt","delete-modify-conflict","conflict-resolution"],"backgroundTag":"delete-modify-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}