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 partialView on GitHub (pinned to 71377f2769)
Solutions
- Read the table-name prefix and the wrapped driver error to identify the failing table and root cause.
- Simply re-run MigrateUp: the sentinel guarantees resume and the rewrite is idempotent, so partial progress is preserved.
- For duplicate-key errors, inspect rows whose derived id collides (rowid derivation in internal/storage/rowid) and resolve duplicate content rows.
- For lock/timeout errors, run the migration during a quiet window or raise lock timeouts, then re-run.
- 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
- Re-run MigrateUp after any failure: the sentinel makes the pass resume and the rewrite is idempotent.
- Run the migration in a quiet window; large aux tables are sensitive to lock timeouts.
- Resolve duplicate content rows before migrating lineages that migrated independently with randomized ids (pre-0037 clones).
- Monitor disk space during the backfill — four full-table rewrites are issued.
- Parse the table-name prefix in the error to target diagnosis instead of re-deriving it.
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
- reading aux rekey sentinel: %w
- recording aux rekey sentinel: %w
- backfill custom tables: %w
- failed to migrate credential keys: %w
- failed to update encrypted password for peer %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/07794d9c44393add.
Report an issue: GitHub.