{"record":{"id":"5df4ae370c1d2608","repo":"gastownhall/beads","slug":"invalid-field-for-update-s","errorCode":null,"errorMessage":"invalid field for update: %s","messagePattern":"invalid field for update: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":426,"sourceCode":"\t\treturn nil, err\n\t}\n\tif crossing {\n\t\tif _, err := EnforceClosePolicyInTx(ctx, tx, id, forceClosePolicy); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := ValidateScalarUpdates(ctx, tx, updates); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Build SET clauses.\n\tsetClauses := []string{\"updated_at = ?\"}\n\targs := []interface{}{time.Now().UTC()}\n\n\tfor key, value := range updates {\n\t\tif !IsAllowedUpdateField(key) {\n\t\t\treturn nil, fmt.Errorf(\"invalid field for update: %s\", key)\n\t\t}\n\n\t\tcolumnName := key\n\t\tif key == \"wisp\" {\n\t\t\tcolumnName = \"ephemeral\"\n\t\t}\n\t\tsetClauses = append(setClauses, fmt.Sprintf(\"`%s` = ?\", columnName))\n\n\t\t// Handle JSON serialization for array fields stored as TEXT.\n\t\tif key == \"waiters\" {\n\t\t\twaitersJSON, _ := json.Marshal(value)\n\t\t\targs = append(args, string(waitersJSON))\n\t\t} else if key == \"metadata\" {\n\t\t\tmetadataStr, err := storage.NormalizeMetadataValue(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid metadata: %w\", err)\n\t\t\t}\n\t\t\targs = append(args, metadataStr)","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L408-L444","documentation":"updateIssueInTx rejects an update map containing a key that is not in the allowlist of updatable fields (IsAllowedUpdateField). This prevents arbitrary or misspelled column names from reaching the SQL SET clause.","triggerScenarios":"Calling UpdateIssueInTx / UpdateIssueWithoutEventInTx with a map key like \"assignee \" (trailing space), a wrong-cased key (\"Assignee\"), a non-column key (\"title_extra\"), or a field that exists on the struct but is not updatable (e.g. \"id\", \"created_at\").","commonSituations":"Refactors renaming struct fields; hand-built update maps with typos; passing computed labels instead of column names; trying to update read-only fields like id or created_at.","solutions":["Check the key against IsAllowedUpdateField (or the allowlist constant) before adding it to the map.","Fix the spelling/case of the field name.","Remove non-updatable fields (id, created_at) from the map; use dedicated APIs for them."],"exampleFix":"// before\nupdates := map[string]interface{}{\"Assignee\": \"alice\"}\n// after\nupdates := map[string]interface{}{\"assignee\": \"alice\"}","handlingStrategy":"validation","validationCode":"for key := range updates {\n    if !issueops.IsAllowedUpdateField(key) {\n        return fmt.Errorf(\"field %q is not updatable\", key)\n    }\n}","typeGuard":"func filterUpdatable(updates map[string]interface{}) map[string]interface{} {\n    out := map[string]interface{}{}\n    for k, v := range updates {\n        if issueops.IsAllowedUpdateField(k) {\n            out[k] = v\n        }\n    }\n    return out\n}","tryCatchPattern":"if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {\n    if strings.Contains(err.Error(), \"invalid field for update\") {\n        return fmt.Errorf(\"update rejected (bad field): %w\", err)\n    }\n    return err\n}","preventionTips":["Run IsAllowedUpdateField on every key before building the map.","Use the same field-name constants as the schema; avoid hand-typed strings.","Remember wisp maps to the ephemeral column internally.","Never include id/created_at in update maps."],"tags":["storage","validation","update-fields"],"backgroundTag":"invalid-update-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}