{"record":{"id":"68aa649bc1f9652a","repo":"gastownhall/beads","slug":"w-cannot-combine-metadata-replacement-with-incre","errorCode":null,"errorMessage":"%w: cannot combine metadata replacement with incremental metadata edits","messagePattern":"%w: cannot combine metadata replacement with incremental metadata edits","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":104,"sourceCode":"\t\tif err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update priority: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.EstimatedMinutes.Set {\n\t\tif err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update estimated_minutes: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.Persistence.Set && !patch.Persistence.Value.IsValid() {\n\t\treturn fmt.Errorf(\"%w: invalid persistence mode %q\", storage.ErrValidation, patch.Persistence.Value)\n\t}\n\treturn nil\n}\n\n// ValidateMetadataPatch checks mutually exclusive metadata edits.\nfunc ValidateMetadataPatch(patch publicops.MetadataPatch) error {\n\tif patch.Replace.Set && (patch.Merge.Set || len(patch.Set) > 0 || len(patch.Unset) > 0) {\n\t\treturn fmt.Errorf(\"%w: cannot combine metadata replacement with incremental metadata edits\", storage.ErrValidation)\n\t}\n\treturn nil\n}\n\n// ValidateScalarUpdates checks typed scalar values before they reach SQL.\nfunc ValidateScalarUpdates(ctx context.Context, tx DBTX, updates map[string]interface{}) error {\n\tif rawType, ok := updates[\"issue_type\"]; ok {\n\t\tvar issueType types.IssueType\n\t\tswitch value := rawType.(type) {\n\t\tcase types.IssueType:\n\t\t\tissueType = value\n\t\tcase string:\n\t\t\tissueType = types.IssueType(value)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"%w: invalid issue type %v\", storage.ErrValidation, rawType)\n\t\t}\n\t\tcustomTypes, err := ResolveCustomTypesInTx(ctx, tx)\n\t\tif err != nil {","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L86-L122","documentation":"ValidateMetadataPatch rejects metadata updates that try to combine full replacement (Replace.Set) with incremental edits (Merge.Set, Set entries, or Unset entries) in the same patch. The two styles are mutually exclusive because their semantics conflict, and the validation runs before ExecuteUpdate writes anything. It wraps storage.ErrValidation.","triggerScenarios":"ExecuteUpdate with a MetadataPatch where Replace.Set=true and any of: Merge.Set=true, len(patch.Set)>0, or len(patch.Unset)>0.","commonSituations":"UI code that always populates a merge/set map for other fields while a separate code path turns on Replace; merging two partial patches client-side so both Replace and Set end up set; copy-pasted request builders combining doc examples of both styles.","solutions":["Choose one style per request: send Replace alone, or send Merge/Set/Unset alone","Split into two sequential ExecuteUpdate calls if both operations are genuinely needed","Reset the unused fields of MetadataPatch to zero values before building the request"],"exampleFix":"// before\npatch := publicops.MetadataPatch{Replace: setField(m), Set: map[string]string{\"k\":\"v\"}} // conflict\n// after\npatch := publicops.MetadataPatch{Replace: setField(m)} // or use Set only, not both","handlingStrategy":"validation","validationCode":"func validMetadataPatch(p publicops.MetadataPatch) bool {\n  if p.Replace.Set && (p.Merge.Set || len(p.Set) > 0 || len(p.Unset) > 0) {\n    return false\n  }\n  return true\n}","typeGuard":"func isReplacementOnly(p publicops.MetadataPatch) bool {\n  return p.Replace.Set && !p.Merge.Set && len(p.Set) == 0 && len(p.Unset) == 0\n}","tryCatchPattern":"if err := issueops.ValidateMetadataPatch(patch); errors.Is(err, storage.ErrValidation) {\n  // split into two calls: one Replace, then one incremental\n}","preventionTips":["Decide replacement vs incremental at one call site; don't let callers mix","Zero out unused MetadataPatch fields when building requests programmatically","Add a table-driven test covering Replace×Merge/Set/Unset combinations"],"tags":["go","validation","metadata","update","mutually-exclusive"],"backgroundTag":"conflicting-update-options","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}