gastownhall/beads · error

retarget to %s collides with existing dependency target in %

Error message

retarget to %s collides with existing dependency target in %s

What it means

This is a deliberate domain error: while retargeting inbound dependencies for id, checkRetargetTargetCollision found that the same (issue_id, target) edge would exist in both its current form and the retargeted form — i.e. an issue already depends on id via the destination column (issue, wisp, or external target). Performing the UPDATE would create a duplicate/conflicting edge, so the operation is aborted with this message instead.

Source

Thrown at internal/storage/issueops/dependencies.go:866

		SELECT 1 FROM %s moving
		JOIN %s existing ON moving.issue_id = existing.issue_id
		WHERE moving.%s = ?
		  AND (existing.%s = ? OR existing.%s = ?)
		LIMIT 1
	`, table, table, sourceCol, conflictCols[0], conflictCols[1])

	var found int
	err := tx.QueryRowContext(ctx, query, id, id, id).Scan(&found)
	if err == sql.ErrNoRows {
		return nil
	}
	if err != nil {
		if isTableNotExistError(err) {
			return nil
		}
		return fmt.Errorf("check retarget collision in %s: %w", table, err)
	}
	return fmt.Errorf("retarget to %s collides with existing dependency target in %s", id, table)
}

//nolint:gosec // G201: table and typedCol are hardcoded constants.
func checkRenameTargetCollision(ctx context.Context, tx DBTX, table, typedCol, newID string) error {
	var otherCols []string
	switch typedCol {
	case "depends_on_issue_id":
		otherCols = []string{"depends_on_wisp_id", "depends_on_external"}
	case "depends_on_wisp_id":
		otherCols = []string{"depends_on_issue_id", "depends_on_external"}
	default:
		return fmt.Errorf("checkRenameTargetCollision: unsupported typed column %q", typedCol)
	}

	query := fmt.Sprintf(`
		SELECT 1 FROM %s a
		JOIN %s b ON a.issue_id = b.issue_id
		WHERE a.%s = ?

View on GitHub (pinned to 71377f2769)

Solutions

  1. Find the conflicting rows: SELECT issue_id FROM <table> WHERE <sourceCol>='id' AND (<destCol>='id' OR depends_on_external='id').
  2. Delete or merge the redundant duplicate edge so only one representation remains, then retry the move.
  3. Run bd doctor / consistency checks to detect and repair duplicated dependency edges.
  4. If caused by a prior partial migration, repair the affected rows before promoting/moving the issue.

Example fix

// before: move blocked by duplicate edge
// bd-7 depends_on bd-42 (issue) AND bd-42 (wisp)
// after: drop the stale duplicate edge first
DELETE FROM dependencies WHERE issue_id='bd-7' AND depends_on_wisp_id='bd-42';
err := MoveIssuePersistenceInTx(ctx, tx, "bd-42")
Defensive patterns

Strategy: validation

Validate before calling

func retargetCollides(ctx context.Context, db *sql.DB, table, id string) (bool, error) {
    const q = `SELECT 1 FROM ` + table + ` moving JOIN ` + table +
        ` existing ON moving.issue_id = existing.issue_id
         WHERE moving.depends_on_issue_id = ?
           AND (existing.depends_on_wisp_id = ? OR existing.depends_on_external = ?) LIMIT 1`
    var one int
    err := db.QueryRowContext(ctx, q, id, id, id).Scan(&one)
    if errors.Is(err, sql.ErrNoRows) { return false, nil }
    return err == nil, err
}
// call before moving; if true, clean the duplicate edge first

Type guard

func isRetargetCollision(err error) bool {
    return err != nil && strings.Contains(err.Error(), "collides with existing dependency target") && strings.Contains(err.Error(), "retarget to")
}

Try / catch

if err := store.MoveIssue(ctx, id); isRetargetCollision(err) {
    // surface the conflicting issue_id from the message and ask the user to dedupe
    return fmt.Errorf("resolve duplicate dependency edges before moving %s: %w", id, err)
}

Prevention

When it happens

Trigger: RetargetInboundDependenciesToWispInTx or RetargetInboundDependenciesToIssueInTx is invoked (via MoveIssuePersistenceInTx / PromoteFromEphemeralInTx) and a row exists where the same issue_id has BOTH the source-column target = id and a destination-column target (depends_on_issue_id / depends_on_wisp_id / depends_on_external) = id.

Common situations: An issue has parallel edges recorded against both representations of the same work item (e.g. after a partial earlier move or manual row edits); duplicate imports created shadow edges; data drift between dependencies and wisp_dependencies from an older buggy version.

Related errors


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