gastownhall/beads · critical

%s: %w

Error message

%s: %w

What it means

During the rekey rewrite, rekeyAuxRowIDsPending rewrites each of the four aux tables (events, comments, issue_snapshots, compaction_snapshots) via rekeyAuxRowTable. If a table's rewrite fails, this error prefixes the table name to the wrapped cause (`<table>: <cause>`), so the operator knows exactly which table's id re-derivation failed. The sentinel stays set, so the next MigrateUp resumes and the rewrite is idempotent — rows already holding derived ids keep them.

Source

Thrown at internal/storage/schema/aux_row_id_backfill.go:255

	// shipped version, but its sentinel proves the rewrite never finished
	// (bd-578h9.16).
	if mainVersionBefore >= pass.shippedMainVersion && !resume {
		return false, nil
	}

	// Sentinel before the first UPDATE: a crash anywhere in the rewrite
	// leaves it set, so the next pass resumes (the rewrite is idempotent)
	// instead of recording the marker over partially re-keyed rows.
	if err := setAuxRekeyInProgress(ctx, db, pass.sentinelKey); err != nil {
		return false, fmt.Errorf("recording aux rekey sentinel: %w", err)
	}

	wrote := false
	for _, t := range auxRekeyTables {
		w, err := rekeyAuxRowTable(ctx, db, t)
		wrote = wrote || w
		if err != nil {
			return wrote, fmt.Errorf("%s: %w", t.name, err)
		}
	}
	if err := clearAuxRekeyInProgress(ctx, db, pass.sentinelKey); err != nil {
		return wrote, fmt.Errorf("clearing aux rekey sentinel: %w", err)
	}
	return wrote, nil
}

// rekeyAuxRowTable re-derives the ids of one table. The whole table is grouped
// by content digest; each digest's rows take the deterministic ids for
// ordinals 0..n-1. A row already holding one of its group's target ids keeps
// it (idempotence: re-running never swaps ids within a group), and the
// remaining rows take the remaining targets in sorted-current-id order. Across
// clones that assignment may permute within a group of exact-duplicate rows,
// but duplicates are interchangeable and the id set is identical, so the
// merged result still converges.
func rekeyAuxRowTable(ctx context.Context, db DBConn, t auxRekeyTable) (bool, error) {
	// Skip cleanly if the table or its id column isn't present (older or partial

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the table-name prefix and the wrapped driver error to identify the failing table and root cause.
  2. Simply re-run MigrateUp: the sentinel guarantees resume and the rewrite is idempotent, so partial progress is preserved.
  3. For duplicate-key errors, inspect rows whose derived id collides (rowid derivation in internal/storage/rowid) and resolve duplicate content rows.
  4. For lock/timeout errors, run the migration during a quiet window or raise lock timeouts, then re-run.
  5. If a table is corrupt, restore it from Dolt history or re-clone before resuming.

Example fix

// before
// rekey fails with: events: Error 1062: Duplicate entry '<id>' for key 'PRIMARY'

// after
-- resolve duplicate content rows in the named table, then:
$ bd migrate   # or just restart bd; the sentinel makes the pass resume idempotently
Defensive patterns

Strategy: retry

Validate before calling

// pre-check for duplicate-prone divergent rows before migrating:
for _, t := range []string{"events", "comments", "issue_snapshots", "compaction_snapshots"} {
    var dupes int
    db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM (SELECT 1 FROM %s GROUP BY issue_id HAVING COUNT(*) > 100) x`, t)).Scan(&dupes)
    _ = dupes // inspect pathological groups before running the rewrite
}

Try / catch

if err := migrateUp(ctx, db); err != nil {
    var table, cause string
    if i := strings.Index(err.Error(), ": "); i > 0 {
        table, cause = err.Error()[:i], err.Error()[i+2:]
    }
    if strings.Contains(cause, "Duplicate entry") {
        return fmt.Errorf("rekey id collision on %s — resolve duplicate content rows, then re-run (resumes idempotently): %w", table, err)
    }
    if strings.Contains(cause, "Lock wait timeout") {
        return retryWithBackoff(func() error { return migrateUp(ctx, db) })
    }
    return err
}

Prevention

When it happens

Trigger: rekeyAuxRowTable failing for one aux table — typically a failed UPDATE/SELECT on that table: lock timeout, duplicate derived id colliding with an existing row, constraint violation, connection drop mid-rewrite, or corrupt row content that breaks the digest derivation.

Common situations: Large aux tables hitting lock/timeout limits during migration; a clone with divergent rows producing an id collision; interrupted earlier run leaving a table half-rekeyed (resume path hits an unexpected state); storage engine errors under disk pressure.

Related errors


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