{"record":{"id":"947987a484cd8991","repo":"gastownhall/beads","slug":"refusing-to-write-unexpected-column-q-of-issues","errorCode":null,"errorMessage":"refusing to write unexpected column %q of issues: %w","messagePattern":"refusing to write unexpected column %q of issues: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/automerge.go","lineNumber":593,"sourceCode":"\t\t\t// side's value was superseded by timestamp. That supersession is\n\t\t\t// otherwise undiagnosable once the conflict row is gone — the same\n\t\t\t// reason the config path names its resolved keys.\n\t\t\tfmt.Fprintf(os.Stderr,\n\t\t\t\t\"Notice: auto-merged issue %v; %s settled last-write-wins (the older side's edit was superseded)\\n\",\n\t\t\t\tm.ourKey, strings.Join(m.lww, \", \"))\n\t\t}\n\t\tif m.ourKey == nil {\n\t\t\treturn fmt.Errorf(\"unexpected conflict row with no issue id (safety check bypassed)\")\n\t\t}\n\t\tif len(m.columns) > 0 {\n\t\t\tsets := make([]string, len(m.columns))\n\t\t\targs := make([]any, 0, len(m.columns)+1)\n\t\t\tfor i, col := range m.columns {\n\t\t\t\t// MySQL cannot bind an identifier and a peer's schema merge can\n\t\t\t\t// extend the conflict table's columns, so gate every name the\n\t\t\t\t// same way the table name is gated.\n\t\t\t\tif err := ValidateConflictTable(col); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"refusing to write unexpected column %q of issues: %w\", col, err)\n\t\t\t\t}\n\t\t\t\tsets[i] = fmt.Sprintf(\"`%s` = ?\", col)\n\t\t\t\targs = append(args, m.values[i])\n\t\t\t}\n\t\t\targs = append(args, m.ourKey)\n\t\t\tstmt := fmt.Sprintf(\"UPDATE `issues` SET %s WHERE `%s` = ?\", strings.Join(sets, \", \"), issuesKeyColumn) //nolint:gosec // identifiers validated above\n\t\t\tres, err := db.ExecContext(ctx, stmt, args...)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"apply merged values for issue %v: %w\", m.ourKey, err)\n\t\t\t}\n\t\t\t// Zero rows would mean the row we planned against is gone —\n\t\t\t// another session deleted it between the read and the write, and\n\t\t\t// clearing the conflict now would discard their side undetectably.\n\t\t\t// But RowsAffected is rows CHANGED, not rows MATCHED: the DSN does\n\t\t\t// not set clientFoundRows (doltutil/dsn.go), so a write the backend\n\t\t\t// normalizes to the bytes already stored also reports zero. Only a\n\t\t\t// follow-up existence check can tell \"vanished\" from \"no-op\".\n\t\t\tif n, err := res.RowsAffected(); err != nil || n == 0 {","sourceCodeStart":575,"sourceCodeEnd":611,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/automerge.go#L575-L611","documentation":"Before building the UPDATE that writes merged values back to `issues`, resolveIssuesFieldMerge validates every column name with ValidateConflictTable (the same identifier gate used for table names), because a peer's schema merge can extend the conflict table's columns with names this code never anticipated. This error wraps that refusal, preventing an unvalidated identifier from being interpolated into SQL.","triggerScenarios":"Auto-resolving issues conflicts when the merge plan contains a column name that fails identifier validation — a peer branch added a column with a name outside the allowed identifier set (odd characters, reserved words, unexpected new columns from a schema merge).","commonSituations":"Merging from a peer running a newer/modified schema whose new issues columns appear in conflict rows; a column named with backticks/quotes or non-identifier characters introduced by another tool.","solutions":["Identify the offending column from the %q in the message","Normalize the schema across branches (align column names to valid identifiers) and re-merge","Resolve the affected conflicts manually with DOLT_CONFLICTS_RESOLVE, then update beads/schema to the current version","If a legitimately new column is being rejected, update beads so its validator allowlists the new schema"],"exampleFix":"// before: peer schema adds column `old-id` (invalid identifier)\n-- merge produces conflict row with column old-id -> refused\n// after: rename the column to a valid identifier on the peer branch\nALTER TABLE issues RENAME COLUMN `old-id` TO `old_id`;","handlingStrategy":"validation","validationCode":"// Reject invalid column names before merging branches\nvar validIdentifier = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)\nfor _, col := range issueColumns {\n    if !validIdentifier.MatchString(col) {\n        return fmt.Errorf(\"column %q is not a valid identifier; rename before merging\", col)\n    }\n}","typeGuard":null,"tryCatchPattern":"err := versioncontrolops.TryAutoResolveMergeConflicts(ctx, db)\nif err != nil {\n    var colErr error\n    if strings.Contains(err.Error(), \"refusing to write unexpected column\") {\n        // identifier gate tripped: align schemas / resolve manually\n        return manualResolve(ctx, db)\n    }\n    return err\n}","preventionTips":["Name all issues-table columns as plain SQL identifiers (letters, digits, underscore)","Avoid renaming or adding columns with reserved/odd characters on peer branches","Keep both repositories on the same beads schema version before merging","Update beads when the canonical schema gains new columns so the validator allowlists them"],"tags":["dolt","merge-conflicts","sql-injection-guard"],"backgroundTag":"sql-identifier-validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}