gastownhall/beads · error

table %s is not union-mergeable

Error message

table %s is not union-mergeable

What it means

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.

Source

Thrown at internal/storage/versioncontrolops/automerge.go:641

		if err != nil {
			return fmt.Errorf("clear conflict for issue %v: %w", m.ourKey, err)
		}
		if n, err := res.RowsAffected(); err == nil && n == 0 {
			return fmt.Errorf("conflict for issue %v was not cleared (no conflict row deleted)", m.ourKey)
		}
	}
	return nil
}

// unionConflictsAreSafe reports whether every live conflict of a union-merged
// table (labels, comments, events) is the same row on both sides with matching
// columns — the only class where "union" has an unambiguous answer. A row
// missing on one side (a deletion racing an insert) or diverging columns in a
// supposedly immutable row goes to the operator.
func unionConflictsAreSafe(ctx context.Context, db DBConn, table string) ([]unionRowKey, bool, error) {
	keyCols, ok := unionConflictKeyColumns[table]
	if !ok {
		return nil, false, fmt.Errorf("table %s is not union-mergeable", table)
	}
	rows, err := loadConflictRows(ctx, db, table)
	if err != nil {
		return nil, false, err
	}
	if declineDuplicateConflictRows(table, keyCols, rows) {
		return nil, false, nil
	}
	plan := make([]unionRowKey, 0, len(rows))
	for _, row := range rows {
		key, ok := unionRowIsSafe(row, keyCols)
		if !ok {
			return nil, false, nil
		}
		plan = append(plan, key)
	}
	return plan, true, nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check that the table name passed to TryAutoResolveMergeConflicts exactly matches a key in unionConflictKeyColumns (labels, comments, events).
  2. If a new union-merged table was added, register its primary-key columns in unionConflictKeyColumns (automerge.go:68) and run TestUnionConflictKeyColumnsCoverTheUnionTables.
  3. For tables like issues that need per-cell resolution, route them to their dedicated resolver (resolveIssuesFieldMerge) instead of the union path.
  4. Otherwise treat it as expected: leave the conflict for manual operator resolution with dolt_conflicts_resolve.

Example fix

// before
 TryAutoResolveMergeConflicts(ctx, db, []string{"labels", "issue_labels"})
// after — only pass allowlisted union tables
 TryAutoResolveMergeConflicts(ctx, db, []string{"labels", "comments", "events"})
Defensive patterns

Strategy: validation

Validate before calling

// only pass union-merged tables
var unionTables = map[string]bool{"labels": true, "comments": true, "events": true}
func canAutoResolve(table string) bool { return unionTables[table] }
if !canAutoResolve(table) { /* skip auto-resolve, leave for manual dolt_conflicts_resolve */ }

Type guard

func isUnionMergeable(table string) bool {
	switch table {
	case "labels", "comments", "events":
		return true
	}
	return false
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/58cf328d253fe9aa. Report an issue: GitHub.