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

  1. Inspect the wrapped cause (%w) at the end of the message; it names the exact failing statement
  2. Check for an existing dependency row whose id already equals depid.New(newID, target) — a duplicate edge causes a PK conflict on rekey
  3. Retry the rename; the whole operation runs in one transaction, so a driver hiccup rolls back cleanly
  4. 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

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


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