gastownhall/beads · error

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

Error message

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

What it means

Deliberate domain error from checkRenameTargetCollision: renaming a dependency target to newID would leave some issue_id holding edges that point to newID through two different target representations at once (typed column being renamed plus the other typed column or the external column). The rename is aborted to prevent duplicate/conflicting dependency edges.

Source

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

		SELECT 1 FROM %s a
		JOIN %s b ON a.issue_id = b.issue_id
		WHERE a.%s = ?
		  AND (b.%s = ? OR b.%s = ?)
		LIMIT 1
	`, table, table, typedCol, otherCols[0], otherCols[1])

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

// RemoveDependencyInTx removes a dependency between two issues within an
// existing transaction. Automatically routes to wisp_dependencies if the
// source issue is an active wisp. When emitEvent is set and a row is actually
// removed it records a dependency_removed event (attributed to actor) on the
// source's event table; a no-op remove of a missing edge, or a structural remove
// with emitEvent unset, records nothing. Only the explicit bd dep remove verb
// sets emitEvent; structural removals (issue delete, reparent, batch, duplicate
// cleanup) leave it unset so they wire edges away silently, mirroring the
// proxied repository's DepInsertOpts.EmitEvent gate so both backends record
// identical history.
//
// It returns whether a dependency_removed event was actually written, so callers
// that stage tables for a Dolt commit stage the events table only when an event
// row exists (avoiding the sweep-unrelated-rows hazard doltAddAndCommit guards
// against, GH#2455).
//

View on GitHub (pinned to 71377f2769)

Solutions

  1. Locate conflicts: SELECT a.issue_id FROM <table> a JOIN <table> b ON a.issue_id=b.issue_id WHERE a.<typedCol>='newID' AND (b.<other>='newID' OR b.depends_on_external='newID').
  2. Remove or rewrite the shadow edge (keep exactly one target representation) before renaming.
  3. Choose a different target ID if the collision stems from an ID naming clash.
  4. Run consistency tooling (bd doctor) to clean duplicate dependency edges after fixing.

Example fix

// before: rename bd-42 -> bd-99 collides
// bd-7 has depends_on_issue_id='bd-42' AND depends_on_external='bd-99'
// after: clear the stale external target first
UPDATE dependencies SET depends_on_external=NULL WHERE issue_id='bd-7' AND depends_on_external='bd-99';
err := store.RenameIssue(ctx, "bd-42", "bd-99")
Defensive patterns

Strategy: validation

Validate before calling

func renameCollides(ctx context.Context, db *sql.DB, table, typedCol, otherCol, newID string) (bool, error) {
    q := "SELECT 1 FROM " + table + " a JOIN " + table + " b ON a.issue_id=b.issue_id WHERE a." + typedCol +
        "=? AND (b." + otherCol + "=? OR b.depends_on_external=?) LIMIT 1"
    var one int
    err := db.QueryRowContext(ctx, q, newID, newID, newID).Scan(&one)
    if errors.Is(err, sql.ErrNoRows) { return false, nil }
    return err == nil, err
}
// run for dependencies/wisp_dependencies before renaming to newID

Type guard

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

Try / catch

if err := store.RenameIssue(ctx, oldID, newID); isRenameCollision(err) {
    return fmt.Errorf("pick a different ID or clear the shadow edge: %w", err)
}

Prevention

When it happens

Trigger: replaceDependencyTargetInTx, the retarget helpers, or UpdateIssueIDInDependencyTargetsInTx (after an issues PK rename) finds a row pair where a.issue_id = b.issue_id, a.<typedCol> = newID, and (b.<otherTypedCol> = newID OR b.depends_on_external = newID) in dependencies or wisp_dependencies.

Common situations: Renaming an issue to an ID that another edge already references via an external/typed target on the same dependent issue; importing issues with IDs that collide with existing external dependency targets; legacy data with both a wisp edge and an issue edge for the same pair.

Related errors


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