{"record":{"id":"58cf328d253fe9aa","repo":"gastownhall/beads","slug":"table-s-is-not-union-mergeable","errorCode":null,"errorMessage":"table %s is not union-mergeable","messagePattern":"table (.+?) is not union-mergeable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/automerge.go","lineNumber":641,"sourceCode":"\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"clear conflict for issue %v: %w\", m.ourKey, err)\n\t\t}\n\t\tif n, err := res.RowsAffected(); err == nil && n == 0 {\n\t\t\treturn fmt.Errorf(\"conflict for issue %v was not cleared (no conflict row deleted)\", m.ourKey)\n\t\t}\n\t}\n\treturn nil\n}\n\n// unionConflictsAreSafe reports whether every live conflict of a union-merged\n// table (labels, comments, events) is the same row on both sides with matching\n// columns — the only class where \"union\" has an unambiguous answer. A row\n// missing on one side (a deletion racing an insert) or diverging columns in a\n// supposedly immutable row goes to the operator.\nfunc unionConflictsAreSafe(ctx context.Context, db DBConn, table string) ([]unionRowKey, bool, error) {\n\tkeyCols, ok := unionConflictKeyColumns[table]\n\tif !ok {\n\t\treturn nil, false, fmt.Errorf(\"table %s is not union-mergeable\", table)\n\t}\n\trows, err := loadConflictRows(ctx, db, table)\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\tif declineDuplicateConflictRows(table, keyCols, rows) {\n\t\treturn nil, false, nil\n\t}\n\tplan := make([]unionRowKey, 0, len(rows))\n\tfor _, row := range rows {\n\t\tkey, ok := unionRowIsSafe(row, keyCols)\n\t\tif !ok {\n\t\t\treturn nil, false, nil\n\t\t}\n\t\tplan = append(plan, key)\n\t}\n\treturn plan, true, nil\n}","sourceCodeStart":623,"sourceCodeEnd":659,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/automerge.go#L623-L659","documentation":"unionConflictsAreSafe only auto-resolves conflicts for tables in the unionConflictKeyColumns allowlist (labels, comments, events). If a table outside that allowlist is passed in, there is no unambiguous 'union' resolution for it, so the function refuses with this error instead of guessing. It is an internal safety guard, not a data problem.","triggerScenarios":"TryAutoResolveMergeConflicts iterates conflicted tables during an automatic merge resolution and reaches a table not present in the unionConflictKeyColumns map (anything other than labels, comments, or events), or a future/renamed table was added to the merge path without a matching allowlist entry.","commonSituations":"A new table was added to the merge flow but the developer forgot to register its key columns in unionConflictKeyColumns; a table name typo or rename broke the map lookup; custom code calls TryAutoResolveMergeConflicts with an unsupported table such as 'issues', which has its own dedicated field-merge resolver.","solutions":["Check that the table name passed to TryAutoResolveMergeConflicts exactly matches a key in unionConflictKeyColumns (labels, comments, events).","If a new union-merged table was added, register its primary-key columns in unionConflictKeyColumns (automerge.go:68) and run TestUnionConflictKeyColumnsCoverTheUnionTables.","For tables like issues that need per-cell resolution, route them to their dedicated resolver (resolveIssuesFieldMerge) instead of the union path.","Otherwise treat it as expected: leave the conflict for manual operator resolution with dolt_conflicts_resolve."],"exampleFix":"// before\n TryAutoResolveMergeConflicts(ctx, db, []string{\"labels\", \"issue_labels\"})\n// after — only pass allowlisted union tables\n TryAutoResolveMergeConflicts(ctx, db, []string{\"labels\", \"comments\", \"events\"})","handlingStrategy":"validation","validationCode":"// only pass union-merged tables\nvar unionTables = map[string]bool{\"labels\": true, \"comments\": true, \"events\": true}\nfunc canAutoResolve(table string) bool { return unionTables[table] }\nif !canAutoResolve(table) { /* skip auto-resolve, leave for manual dolt_conflicts_resolve */ }","typeGuard":"func isUnionMergeable(table string) bool {\n\tswitch table {\n\tcase \"labels\", \"comments\", \"events\":\n\t\treturn true\n\t}\n\treturn false\n}","tryCatchPattern":null,"preventionTips":["Only call TryAutoResolveMergeConflicts with tables listed in unionConflictKeyColumns.","When adding a new union-merged table, add its key columns to the allowlist and keep TestUnionConflictKeyColumnsCoverTheUnionTables passing.","Route issues-table conflicts to the dedicated field-merge resolver, not the union path.","Treat this error as a signal to fall back to manual dolt_conflicts_resolve."],"tags":["go","merge-conflicts","allowlist","dolt"],"backgroundTag":"table-not-union-mergeable","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}