gastownhall/beads · error

scan conflict row for table %s: %w

Error message

scan conflict row for table %s: %w

What it means

loadConflictRows scans each conflict row into generic []any pointers; this error wraps a rows.Scan failure for a specific table. Scanning fails when a column's value cannot be converted into a generic any slot — typically NULL-shape or type issues in the conflict row produced by dolt, or column-count mismatch when a peer's schema merge changed the conflicts table shape.

Source

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

	rows, err := db.QueryContext(ctx, "SELECT * FROM `dolt_conflicts_"+table+"`") //nolint:gosec // table validated as an identifier above
	if err != nil {
		return nil, fmt.Errorf("query conflicts for table %s: %w", table, err)
	}
	defer func() { _ = rows.Close() }()

	cols, err := rows.Columns()
	if err != nil {
		return nil, fmt.Errorf("conflict columns for table %s: %w", table, err)
	}
	var out []rawConflictRow
	for rows.Next() {
		vals := make([]any, len(cols))
		ptrs := make([]any, len(cols))
		for i := range vals {
			ptrs[i] = &vals[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			return nil, fmt.Errorf("scan conflict row for table %s: %w", table, err)
		}
		out = append(out, rawConflictRow{cols: cols, vals: vals})
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate conflicts for table %s: %w", table, err)
	}
	return out, nil
}

// duplicateConflictKey reports the first our-side key held by more than one
// live conflict row. Both resolvers settle a row by deleting its conflict BY
// KEY, so two rows sharing one key would both be cleared by the first delete
// and make the second iteration abort on "no conflict row deleted" — a message
// about the wrong thing entirely, after a row was resolved without ever being
// merged. loadConflictRow refuses the same shape on the operator's single-row
// path (conflicts.go); the auto-merge pre-screens instead DECLINE on it, which
// is this file's idiom and what lets the caller still build the
// MergeConflictsError that tells an operator which tables need them.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause to see which column/type failed to scan
  2. Abort and resolve the merge manually with dolt's table-level resolution (DOLT_CONFLICTS_RESOLVE --ours/--theirs), then retry
  3. Align both branches' schemas before merging so conflict rows keep the expected shape
  4. Upgrade dolt/beads if the conflict-row format changed between versions
Defensive patterns

Strategy: fallback

Validate before calling

// Check the conflict table's column count matches the base table's
// expected shape before auto-resolve
rows, _ := db.QueryContext(ctx, "SELECT * FROM dolt_conflicts_issues LIMIT 1")
cols, _ := rows.Columns()
rows.Close()
if len(cols) < expectedMinConflictColumns { /* shape drifted; use manual resolution */ }

Try / catch

err := versioncontrolops.TryAutoResolveMergeConflicts(ctx, db)
if err != nil && strings.Contains(err.Error(), "scan conflict row for table") {
    // conflict-row shape unexpected (schema merge): fall back to
    // manual table-level resolution
    return manualResolve(ctx, db)
}

Prevention

When it happens

Trigger: Auto-resolve reads dolt_conflicts_issues (or labels/comments/events) and a row cannot be scanned — a peer's schema merge extended the conflict table's columns so the scan buffer mismatches, or the conflict row contains a value type the driver will not scan into any.

Common situations: Merging branches where the schema itself changed (new columns) so dolt_conflicts tables carry extra base/our/their metadata; corrupted conflict rows after a crash.

Related errors


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