gastownhall/beads · error

insert replacement dependency target: %w

Error message

insert replacement dependency target: %w

What it means

This error wraps failure of the reinsert INSERT in replaceDependencyTargetInTx, which re-adds each edge with a freshly derived deterministic key depid.New(issueID, newID) so the generated depends_on_id and clone-stable primary key (#4259) stay correct. It typically means a primary/unique key conflict — an edge with the same (issue_id, newID) key already exists — or a constraint/check violation.

Source

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

	_ = queryRows.Close()
	if err := queryRows.Err(); err != nil {
		return fmt.Errorf("iterate dependency targets: %w", err)
	}

	//nolint:gosec // table and column are hardcoded by callers.
	if _, err := tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE %s = ? OR (%s = ? AND depends_on_external IS NULL)`, table, column, DepTargetExpr), oldID, oldID); err != nil {
		return fmt.Errorf("delete old dependency target: %w", err)
	}
	for _, row := range rows {
		// The retargeted edge's natural key is (issue_id, newID): the switch above
		// set exactly one typed target column to newID. Re-derive id from it so the
		// rewritten row stays merge-safe and keeps a clone-stable primary key (#4259).
		//nolint:gosec // table is hardcoded by callers.
		if _, err := tx.ExecContext(ctx, fmt.Sprintf(`
			INSERT INTO %s (id, issue_id, depends_on_issue_id, depends_on_wisp_id, depends_on_external, type, created_at, created_by, metadata, thread_id)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		`, table), depid.New(row.issueID, newID), row.issueID, nullStringValue(row.issueTarget), nullStringValue(row.wispTarget), nullStringValue(row.external), row.depType, nullTimeValue(row.createdAt), nullStringValue(row.createdBy), nullStringValue(row.metadata), nullStringValue(row.threadID)); err != nil {
			return fmt.Errorf("insert replacement dependency target: %w", err)
		}
	}
	return nil
}

func nullStringValue(value sql.NullString) any {
	if !value.Valid {
		return nil
	}
	return value.String
}

func nullTimeValue(value sql.NullTime) any {
	if !value.Valid {
		return nil
	}
	return value.Time
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped driver error; `Duplicate entry` confirms an existing edge with the derived id
  2. Deduplicate: delete the pre-existing colliding edge, then retry the rename
  3. Check constraint violations by inspecting the row's three target columns — exactly one must be set
  4. Retry within the transaction; failed renames roll back, leaving original edges intact

Example fix

// before: both issues depend on bd-200; retarget inserts a duplicate id
INSERT INTO dependencies (id, issue_id, depends_on_issue_id, ...) VALUES ('bd-100->bd-200', 'bd-100', 'bd-200', ...);
-- Error 1062: Duplicate entry 'bd-100->bd-200'
// after: drop the pre-existing edge first
DELETE FROM dependencies WHERE issue_id='bd-100' AND depends_on_issue_id='bd-200';
-- then retry the rename so the reinsert succeeds
Defensive patterns

Strategy: validation

Validate before calling

// Ensure no edge already exists under the post-rename deterministic id
var dupes int
db.Get(&dupes, `SELECT COUNT(*) FROM dependencies d
  WHERE d.issue_id IN (SELECT depends_on_issue_id FROM dependencies WHERE depends_on_issue_id = ?)
    AND d.id = CONCAT(d.issue_id, '->', ?)`, oldID, newID)
_ = dupes // or, simply: dedupe edges to the shared target before renaming
// Check one-target invariant on rows being moved
var bad int
db.Get(&bad, `SELECT COUNT(*) FROM dependencies WHERE depends_on_issue_id = ?
  AND (depends_on_wisp_id IS NOT NULL OR depends_on_external IS NOT NULL)`, oldID)
if bad > 0 { return fmt.Errorf("%d rows violate one-target check; repair before rename", bad) }

Try / catch

err := updateIssueOrWispID(tx, oldID, newID)
if err != nil {
    var dup *mysql.MySQLError
    if errors.As(err, &dup) && dup.Number == 1062 {
        return fmt.Errorf("edge already exists after retarget (%s); dedupe and retry: %w", dup.Message, err)
    }
    return err
}

Prevention

When it happens

Trigger: UpdateIssueIDInTx / UpdateWispIDInTx rename where the retargeted edge's id depid.New(row.issueID, newID) already exists (duplicate edge to the renamed target), or a row violates ck_dep_one_target / NOT NULL constraints after the target-column switch.

Common situations: Renaming issue A to B when both A and B already depend on the same target; merge/clone divergence leaving both old- and new-key variants of an edge; corrupt rows with all three target columns NULL hitting the one-target check.

Related errors


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