gastownhall/beads · error
rekey dependency sources %s -> %s: %w
Error message
rekey dependency sources %s -> %s: %w
What it means
This error wraps any failure from rekeyDependencySourceInTx while renaming an issue's ID inside UpdateIssueIDInDependenciesInTx. After the FK cascade renames dependencies.issue_id, the stored surrogate primary key id still equals depid.New(oldID, target); the library re-derives it as depid.New(newID, target) to keep the same-PK=>same-edge invariant across clones (#4259). When that rekey step fails for any reason (query, scan, iterate, or update errors bubbling up), it is wrapped with this message.
Source
Thrown at internal/storage/issueops/dependencies.go:626
}
return nil
}
func UpdateIssueIDInDependenciesInTx(ctx context.Context, tx *sql.Tx, oldID, newID string) error {
for _, table := range []string{"dependencies", "wisp_dependencies"} {
if err := replaceDependencyTargetInTx(ctx, tx, table, "depends_on_issue_id", oldID, newID); err != nil {
return fmt.Errorf("update issue target %s -> %s in %s: %w", oldID, newID, table, err)
}
}
// Re-derive the deterministic primary key for rows whose SOURCE issue was
// renamed. dependencies.issue_id carries fk_dep_issue ... ON UPDATE CASCADE, so
// renaming the issues row (updateIssueIDInTx updates issues.id first) cascades
// issue_id from oldID to newID before we get here — but the cascade leaves the
// surrogate id at depid.New(oldID, target). A stale id re-forks the primary key
// across clones (#4259) and breaks the same-PK => same-edge invariant the pull
// conflict resolver relies on, so recompute it from the post-rename (newID, target).
if err := rekeyDependencySourceInTx(ctx, tx, oldID, newID); err != nil {
return fmt.Errorf("rekey dependency sources %s -> %s: %w", oldID, newID, err)
}
return nil
}
// rekeyDependencySourceInTx rewrites dependencies.id for every edge whose source
// issue was renamed to newID so the stored id equals depid.New(newID, target). It
// matches rows by both newID (the normal post-FK-cascade state) and oldID (defensive,
// in case a caller reaches here before the cascade) and re-asserts issue_id = newID
// so the row converges either way. Only rows whose id is actually stale are touched.
func rekeyDependencySourceInTx(ctx context.Context, tx *sql.Tx, oldID, newID string) error {
queryRows, err := tx.QueryContext(ctx, `
SELECT id, depends_on_issue_id, depends_on_wisp_id, depends_on_external
FROM dependencies
WHERE issue_id = ? OR issue_id = ?
`, newID, oldID)
if err != nil {
return fmt.Errorf("query dependency sources: %w", err)
}View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause (%w) at the end of the message; it names the exact failing statement
- Check for an existing dependency row whose id already equals depid.New(newID, target) — a duplicate edge causes a PK conflict on rekey
- Retry the rename; the whole operation runs in one transaction, so a driver hiccup rolls back cleanly
- Verify the dependencies table schema is current (bd migrate / upgrade) since generated depends_on_id columns are involved
Example fix
// before: renaming to an ID that duplicates an existing edge bd update bd-101 --id bd-100 // fails: rekey dependency sources bd-101 -> bd-100: ... duplicate entry // after: resolve the duplicate edge first, then rename bd dep remove bd-100 --depends-on bd-200 // drop the edge that would collide bd update bd-101 --id bd-100
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check for rekey collisions before renaming an issue
rows := []struct{ ID, IssueID string }{}
err := db.Select(&rows, `SELECT id, issue_id FROM dependencies WHERE issue_id IN (?, ?)`, oldID, newID)
if err != nil { return err }
ids := map[string]bool{}
for _, r := range rows {
if ids[r.ID] { return fmt.Errorf("duplicate dependency row id %s will block rekey", r.ID) }
ids[r.ID] = true
} Try / catch
err := updateIssueID(tx, oldID, newID)
if err != nil {
var dup *mysql.MySQLError
if errors.As(err, &dup) && dup.Number == 1062 {
// duplicate dependency key: dedupe edges to the same target, then retry
}
return fmt.Errorf("issue rename failed, transaction rolled back: %w", err)
} Prevention
- Keep one bd process per database at a time — concurrent renames race on dependency rows
- Run bd doctor / migrations before bulk renames so schema and generated columns are current
- Treat the rename as atomic: on failure just retry, never hand-patch dependency ids
- Before renaming to an existing ID, check for edges from both old and new issues to the same targets
When it happens
Trigger: Calling UpdateIssueIDInTx (issue rename) when any dependency edge references the renamed issue as a source and rekeyDependencySourceInTx fails: the SELECT on dependencies fails, a row fails to scan, rows.Err() reports a driver error, or the UPDATE dependencies SET id = ... hits a primary/unique key conflict.
Common situations: Renaming an issue whose ID collides with an existing deterministic dependency key (duplicate edge to the same target), a transient Dolt/MySQL driver failure mid-transaction, or a corrupted dependency row missing all three target columns.
Related errors
- rekey dependency source id %s -> %s: %w
- insert replacement dependency target: %w
- ErrTransaction
- open unit of work: %w
- failed to begin transaction: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/36cef804b9da6091.
Report an issue: GitHub.